Spring Boot Command Pattern Example

By Ercan - 21/10/2025

The Command Pattern is one of the most versatile and widely used behavioral design patterns. It encapsulates a request as an object, allowing you to parameterize, queue, and execute actions dynamically — all without tightly coupling the caller to the actual logic being executed.

In this article, we’ll explore how to apply the Command Pattern in a real-world Spring Boot project, where we execute different text encoding and decoding algorithms such as Caesar, Atbash, and ROT13.

This example is designed to be both educational and practical — ideal for developers who want to deeply understand the pattern beyond theory.

TL;DR: If you want to skip the explanation and dive straight into the code, check out the full implementation on GitHub: https://github.com/ercansormaz/command-pattern


Core Idea of the Command Pattern

The Command Pattern decouples the invoker (the object that triggers an action) from the receiver (the object that performs it).
Each command encapsulates:

  • The action to perform
  • The receiver of that action
  • The parameters needed to perform it

This separation provides flexibility for executing, batching, or even undoing operations without changing the invoker’s logic.


How It Works

1. Command Interface

Every command implements a common interface:

public interface CryptoCommand {
  String execute();
}

2. Concrete Commands

Each algorithm has its own command class, e.g.:

@RequiredArgsConstructor
public class CaesarEncodeCryptoCommand implements CryptoCommand {

  private final CaesarCipher chiper;
  private final CryptoCommandInput input;

  @Override
  public String execute() {
    return chiper.encode(input.getInput(), input.getShift());
  }

}

3. Factories

To decouple instantiation, each algorithm has a factory:

@Component
@RequiredArgsConstructor
public class CaesarEncodeCryptoCommandFactory implements CryptoCommandFactory {

  private final CaesarCipher caesarCipher;

  @Override
  public CryptoCommand create(CryptoCommandInput input) {
    return new CaesarEncodeCryptoCommand(caesarCipher, input);
  }

  @Override
  public CryptoMethod getMethod() {
    return CryptoMethod.CAESAR;
  }

  @Override
  public List<CryptoOperation> getSupportedOperations() {
    return List.of(CryptoOperation.ENCODE);
  }
}

4. Invoker

The CryptoCommandInvoker dynamically maps all factories at startup:

  @PostConstruct
  public void prepareCommandMap() {
    cryptoCommandFactories.forEach(factory -> {
      Map<CryptoOperation, CryptoCommandFactory> innerMap = commandFactoryMap.computeIfAbsent(
          factory.getMethod(), m -> new EnumMap<>(CryptoOperation.class));

      factory.getSupportedOperations().forEach(op -> innerMap.putIfAbsent(op, factory));
    });
  }

  private CryptoCommand getCommand(CryptoCommandInput input) {
    Map<CryptoOperation, CryptoCommandFactory> byMethod = commandFactoryMap.get(input.getMethod());
    if (byMethod == null) {
      throw new IllegalArgumentException("Unsupported method: " + input.getMethod());
    }

    CryptoCommandFactory factory = byMethod.get(input.getOperation());
    if (factory == null) {
      throw new IllegalArgumentException(
          "Unsupported operation: " + input.getOperation() + " for method: " + input.getMethod());
    }

    return factory.create(input);
  }

This removes all switch-case logic and keeps the system open for extension — just add a new factory and command class, and the invoker will pick it up automatically.


REST Endpoints

The project exposes three endpoints to showcase different command execution scenarios.

🔹 Single Command

POST /crypto
{
  "operation": "encode",
  "method": "caesar",
  "text": "hello",
  "shift": 3
}

Response:

{
  "operation": "ENCODE",
  "method": "CAESAR",
  "result": "khoor",
  "shift": 3
}

🔹 Batch Commands

POST /crypto/batch
{
  "commands": [
    {"operation":"encode", "method":"caesar", "text":"hello", "shift":3},
    {"operation":"encode", "method":"rot13", "text":"hello"},
    {"operation":"encode", "method":"atbash", "text":"hello"}
  ]
}

Executes multiple commands sequentially and returns an array of results.

🔹 Pipeline Commands

POST /crypto/pipeline
{
  "text": "hello world",
  "commands": [
    {"operation":"encode", "method":"caesar", "shift":3},
    {"operation":"encode", "method":"rot13"},
    {"operation":"encode", "method":"atbash"}
  ]
}

Each command’s output becomes the next command’s input — a great example of command chaining.


What Makes This Example Stand Out

  • Decoupled Design: The controller, invoker, and commands operate independently.
  • Extensibility: Adding a new algorithm only requires a new command + factory.
  • Batch & Pipeline Execution: Demonstrates real-world flexibility of the Command pattern.
  • Spring Integration: Uses Spring components for discovery and runtime mapping.

Takeaways

This project shows how the Command Pattern can elegantly handle dynamic operations — whether they’re single, grouped, or chained.
It’s not just a theoretical example, but a practical demonstration of how design patterns improve scalability and clarity in modern Spring Boot applications.

👉 You can explore the full source code on GitHub:
https://github.com/ercansormaz/command-pattern

Tags: spring boot, design pattern, command pattern, java