<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Jitin</title>
    <description>The latest articles on Forem by Jitin (@jitin_800e8b484929929663a).</description>
    <link>https://forem.com/jitin_800e8b484929929663a</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3690410%2Ff0bd4b5d-12e8-4ac0-9ba9-33e4a7c57dee.png</url>
      <title>Forem: Jitin</title>
      <link>https://forem.com/jitin_800e8b484929929663a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jitin_800e8b484929929663a"/>
    <language>en</language>
    <item>
      <title>The Command Pattern Simplified: How Modern Java (21–25) Makes It Elegant</title>
      <dc:creator>Jitin</dc:creator>
      <pubDate>Thu, 08 Jan 2026 13:58:21 +0000</pubDate>
      <link>https://forem.com/jitin_800e8b484929929663a/the-command-pattern-simplified-how-modern-java-21-25-makes-it-elegant-2fjh</link>
      <guid>https://forem.com/jitin_800e8b484929929663a/the-command-pattern-simplified-how-modern-java-21-25-makes-it-elegant-2fjh</guid>
      <description>&lt;p&gt;The Command pattern is one of those classic design patterns that feels both brilliant and tedious.&lt;br&gt;
Its idea is simple: capture a request as an object so you can parameterise clients, queue requests, and support undoable operations.&lt;/p&gt;

&lt;p&gt;But implementing it? That’s where things get messy. You end up with dozens of classes — one for each command. Command interface, concrete commands, receivers, invokers… the boilerplate never ends.&lt;/p&gt;

&lt;p&gt;Over the last few Java releases, though, the language has evolved.&lt;br&gt;
With records, sealed interfaces and pattern matching, modern Java (21 through 25) makes the Command pattern not just easier, but beautifully elegant.&lt;/p&gt;

&lt;p&gt;The Problem: Traditional Command Pattern Verbosity&lt;br&gt;
Let me show you what implementing the Command pattern looked like before Java 17.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The Old Way: A Text Editor with Undo/Redo
// ❌ Traditional Command Pattern (Java 17 and before) - LOTS OF BOILERPLATE

// Step 1: Command interface
public interface EditorCommand {
    void execute();
    void undo();
}

// Step 2: Receiver - the actual editor
public class TextEditor {
    private StringBuilder text = new StringBuilder();

    public void insertText(String str) {
        text.append(str);
    }

    public void deleteText(int length) {
        if (text.length() &amp;gt;= length) {
            text.delete(text.length() - length, text.length());
        }
    }

    public String getContent() {
        return text.toString();
    }
}

// Step 3: Concrete command for inserting text
public class InsertCommand implements EditorCommand {
    private TextEditor editor;
    private String textToInsert;
    private int position;

    public InsertCommand(TextEditor editor, String text, int position) {
        this.editor = editor;
        this.textToInsert = text;
        this.position = position;
    }

    @Override
    public void execute() {
        editor.insertText(textToInsert);
    }

    @Override
    public void undo() {
        editor.deleteText(textToInsert.length());
    }
}

// Step 4: Concrete command for deleting text
public class DeleteCommand implements EditorCommand {
    private TextEditor editor;
    private String deletedText;
    private int deleteLength;

    public DeleteCommand(TextEditor editor, int length) {
        this.editor = editor;
        this.deleteLength = length;
    }

    @Override
    public void execute() {
        // First, save what we're deleting (for undo)
        deletedText = editor.getContent();
        editor.deleteText(deleteLength);
    }

    @Override
    public void undo() {
        // Restore the deleted text
        editor.insertText(deletedText);
    }
}

// Step 5: More concrete commands...
public class FindAndReplaceCommand implements EditorCommand {
    private TextEditor editor;
    private String searchText;
    private String replaceText;
    private String previousContent;

    public FindAndReplaceCommand(TextEditor editor, String search, String replace) {
        this.editor = editor;
        this.searchText = search;
        this.replaceText = replace;
    }

    @Override
    public void execute() {
        previousContent = editor.getContent();
        // Implementation...
    }

    @Override
    public void undo() {
        // Restore previous state
    }
}

// Step 6: Invoker - manages command execution history
public class CommandHistory {
    private Stack&amp;lt;EditorCommand&amp;gt; undoStack = new Stack&amp;lt;&amp;gt;();
    private Stack&amp;lt;EditorCommand&amp;gt; redoStack = new Stack&amp;lt;&amp;gt;();

    public void execute(EditorCommand command) {
        command.execute();
        undoStack.push(command);
        redoStack.clear();  // Clear redo history on new command
    }

    public void undo() {
        if (!undoStack.isEmpty()) {
            EditorCommand command = undoStack.pop();
            command.undo();
            redoStack.push(command);
        }
    }

    public void redo() {
        if (!redoStack.isEmpty()) {
            EditorCommand command = redoStack.pop();
            command.execute();
            undoStack.push(command);
        }
    }
}

// Usage
public static void main(String[] args) {
    TextEditor editor = new TextEditor();
    CommandHistory history = new CommandHistory();

    history.execute(new InsertCommand(editor, "Hello", 0));
    history.execute(new InsertCommand(editor, " World", 5));
    System.out.println(editor.getContent());  // "Hello World"

    history.undo();
    System.out.println(editor.getContent());  // "Hello"

    history.redo();
    System.out.println(editor.getContent());  // "Hello World"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Problems:&lt;/p&gt;

&lt;p&gt;One class per command — dozens in large systems&lt;br&gt;
Repeated execute() / undo() boilerplate&lt;br&gt;
Hard to maintain or extend&lt;br&gt;
Type-unsafe, and often error-prone&lt;br&gt;
🚀 The Modern Way: Records + Sealed Interfaces + Pattern Matching&lt;br&gt;
By Java 25, all the language features we need for an elegant Command pattern are stable:&lt;/p&gt;

&lt;p&gt;✅ Records — immutable data containers with auto-generated boilerplate&lt;br&gt;
✅ Sealed interfaces — restrict which command types can exist&lt;br&gt;
✅ Pattern matching for switch — exhaustive, type-safe command handling&lt;br&gt;
Let’s rebuild the same editor example with these tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Step 1: Define command as a sealed interface with record implementations
public sealed interface EditorCommand {
    void execute(TextEditor editor);
    void undo(TextEditor editor);

    // Command to insert text
    record Insert(String text, int position) implements EditorCommand {
        @Override
        public void execute(TextEditor editor) {
            editor.insertText(text);
        }

        @Override
        public void undo(TextEditor editor) {
            editor.deleteText(text.length());
        }
    }

    // Command to delete text
    record Delete(int length, String deletedContent) implements EditorCommand {
        @Override
        public void execute(TextEditor editor) {
            editor.deleteText(length);
        }

        @Override
        public void undo(TextEditor editor) {
            editor.insertText(deletedContent);
        }
    }

    // Command to find and replace
    record Replace(String find, String replaceWith, String previousContent) implements EditorCommand {
        @Override
        public void execute(TextEditor editor) {
            editor.findAndReplace(find, replaceWith);
        }

        @Override
        public void undo(TextEditor editor) {
            // Restore previous state
            editor.setContent(previousContent);
        }
    }
}

// Step 2: Simple receiver (same as before)
public class TextEditor {
    private StringBuilder text = new StringBuilder();

    public void insertText(String str) {
        text.append(str);
    }

    public void deleteText(int length) {
        if (text.length() &amp;gt;= length) {
            text.delete(text.length() - length, text.length());
        }
    }

    public String getContent() {
        return text.toString();
    }

    public void setContent(String content) {
        text = new StringBuilder(content);
    }

    public void findAndReplace(String find, String replaceWith) {
        String content = text.toString();
        text = new StringBuilder(content.replace(find, replaceWith));
    }
}

// Step 3: Invoker with pattern matching
public class CommandHistory {
    private Stack&amp;lt;EditorCommand&amp;gt; undoStack = new Stack&amp;lt;&amp;gt;();
    private Stack&amp;lt;EditorCommand&amp;gt; redoStack = new Stack&amp;lt;&amp;gt;();
    private TextEditor editor;

    public CommandHistory(TextEditor editor) {
        this.editor = editor;
    }

    public void execute(EditorCommand command) {
        // Use pattern matching to capture state before execution if needed
        switch (command) {
            case EditorCommand.Delete(int length, _) -&amp;gt; {
                // Before executing delete, update the deletedContent
                String content = editor.getContent();
                EditorCommand updated = new EditorCommand.Delete(length, content);
                updated.execute(editor);
                undoStack.push(updated);
            }
            case EditorCommand.Replace(String find, String replaceWith, _) -&amp;gt; {
                String content = editor.getContent();
                EditorCommand updated = new EditorCommand.Replace(find, replaceWith, content);
                updated.execute(editor);
                undoStack.push(updated);
            }
            case EditorCommand.Insert _ -&amp;gt; {
                command.execute(editor);
                undoStack.push(command);
            }
        }
        redoStack.clear();
    }

    public void undo() {
        if (!undoStack.isEmpty()) {
            EditorCommand command = undoStack.pop();
            command.undo(editor);
            redoStack.push(command);
        }
    }

    public void redo() {
        if (!redoStack.isEmpty()) {
            EditorCommand command = redoStack.pop();
            command.execute(editor);
            undoStack.push(command);
        }
    }
}

// Usage - Much cleaner!
public static void main(String[] args) {
    TextEditor editor = new TextEditor();
    CommandHistory history = new CommandHistory(editor);

    history.execute(new EditorCommand.Insert("Hello", 0));
    history.execute(new EditorCommand.Insert(" World", 5));
    System.out.println(editor.getContent());  // "Hello World"

    history.undo();
    System.out.println(editor.getContent());  // "Hello"

    history.redo();
    System.out.println(editor.getContent());  // "Hello World"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What improved:&lt;/p&gt;

&lt;p&gt;✅ Fewer than 60 lines&lt;br&gt;
✅ Immutable, type-safe commands&lt;br&gt;
✅ All logic in one file&lt;br&gt;
✅ Compiler-enforced exhaustiveness&lt;/p&gt;

&lt;p&gt;Advanced: Combining with Lambdas for Task Queues&lt;br&gt;
Records and lambdas make functional-style commands possible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ✅ Java 21+: Functional command pattern with sealed interfaces and records

// Sealed command interface
public sealed interface Task {
    void run();

    // Simple task - wraps a lambda
    record SimpleTask(String name, Runnable action) implements Task {
        @Override
        public void run() {
            System.out.println("Executing: " + name);
            action.run();
        }
    }

    // Async task with timeout
    record AsyncTask(
        String name,
        Runnable action,
        long timeoutMs
    ) implements Task {
        @Override
        public void run() {
            System.out.println("Executing async: " + name);
            Thread task = new Thread(action);
            task.start();
            try {
                task.join(timeoutMs);
                if (task.isAlive()) {
                    System.out.println("Task timed out!");
                    task.interrupt();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    // Conditional task
    record ConditionalTask(
        String name,
        Supplier&amp;lt;Boolean&amp;gt; condition,
        Runnable ifTrue,
        Runnable ifFalse
    ) implements Task {
        @Override
        public void run() {
            System.out.println("Executing conditional: " + name);
            if (condition.get()) {
                ifTrue.run();
            } else {
                ifFalse.run();
            }
        }
    }
}

// Task queue executor with pattern matching
public class TaskQueue {
    private Queue&amp;lt;Task&amp;gt; tasks = new LinkedList&amp;lt;&amp;gt;();

    public void enqueue(Task task) {
        tasks.add(task);
    }

    public void executeTasks() {
        while (!tasks.isEmpty()) {
            Task task = tasks.poll();

            // Pattern matching for different task types
            switch (task) {
                case Task.SimpleTask(String name, Runnable action) -&amp;gt; {
                    System.out.println("[SIMPLE] " + name);
                    action.run();
                }

                case Task.AsyncTask(String name, Runnable action, long timeout) -&amp;gt; {
                    System.out.println("[ASYNC] " + name + " (timeout: " + timeout + "ms)");
                    // Execute with timeout...
                }

                case Task.ConditionalTask(String name, var condition, var ifTrue, var ifFalse) -&amp;gt; {
                    System.out.println("[CONDITIONAL] " + name);
                    if (condition.get()) {
                        ifTrue.run();
                    } else {
                        ifFalse.run();
                    }
                }
            }
        }
    }
}

// Usage - Minimal code, maximum expressiveness
public static void main(String[] args) {
    TaskQueue queue = new TaskQueue();

    // Add simple task using lambda
    queue.enqueue(new Task.SimpleTask(
        "Print greeting",
        () -&amp;gt; System.out.println("Hello from task queue!")
    ));

    // Add async task
    queue.enqueue(new Task.AsyncTask(
        "Download data",
        () -&amp;gt; System.out.println("Downloading..."),
        5000  // 5 second timeout
    ));

    // Add conditional task
    queue.enqueue(new Task.ConditionalTask(
        "Check network",
        () -&amp;gt; isNetworkAvailable(),
        () -&amp;gt; System.out.println("Connected!"),
        () -&amp;gt; System.out.println("No connection")
    ));

    // Execute all tasks
    queue.executeTasks();
}

private static boolean isNetworkAvailable() {
    return true;  // Simplified
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🏦 Real Example: Payment Commands&lt;br&gt;
Let’s apply this to something closer to real-world business logic — a banking system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ✅ Payment command system with undo/redo

public sealed interface PaymentCommand {
    void execute(Account account);
    void undo(Account account);

    record Deposit(double amount) implements PaymentCommand {
        public Deposit {
            if (amount &amp;lt;= 0) throw new IllegalArgumentException("Amount must be positive");
        }

        @Override
        public void execute(Account account) {
            account.addBalance(amount);
        }

        @Override
        public void undo(Account account) {
            account.subtractBalance(amount);
        }
    }

    record Withdraw(double amount, double previousBalance) implements PaymentCommand {
        public Withdraw(double amount) {
            this(amount, 0);
        }

        @Override
        public void execute(Account account) {
            if (account.getBalance() &amp;lt; amount) {
                throw new IllegalArgumentException("Insufficient funds");
            }
            account.subtractBalance(amount);
        }

        @Override
        public void undo(Account account) {
            account.setBalance(previousBalance);
        }
    }

    record Transfer(
        Account from,
        Account to,
        double amount
    ) implements PaymentCommand {
        @Override
        public void execute(Account account) {
            from.subtractBalance(amount);
            to.addBalance(amount);
        }

        @Override
        public void undo(Account account) {
            to.subtractBalance(amount);
            from.addBalance(amount);
        }
    }

    record FeesApplied(
        double feeAmount,
        String reason
    ) implements PaymentCommand {
        @Override
        public void execute(Account account) {
            account.subtractBalance(feeAmount);
        }

        @Override
        public void undo(Account account) {
            account.addBalance(feeAmount);
        }
    }
}

// Account receiver
public class Account {
    private String accountNumber;
    private double balance;
    private List&amp;lt;PaymentCommand&amp;gt; history = new ArrayList&amp;lt;&amp;gt;();

    public Account(String number, double initialBalance) {
        this.accountNumber = number;
        this.balance = initialBalance;
    }

    public void processCommand(PaymentCommand command) {
        command.execute(this);
        history.add(command);
    }

    public void undoLastCommand() {
        if (!history.isEmpty()) {
            PaymentCommand last = history.remove(history.size() - 1);
            last.undo(this);
        }
    }

    // Receiver methods
    public void addBalance(double amount) { balance += amount; }
    public void subtractBalance(double amount) { balance -= amount; }
    public void setBalance(double amount) { balance = amount; }
    public double getBalance() { return balance; }

    public void printStatement() {
        System.out.println("Account: " + accountNumber);
        System.out.println("Balance: $" + String.format("%.2f", balance));
        System.out.println("Transactions: " + history.size());
    }
}

// Usage
public static void main(String[] args) {
    Account checking = new Account("CHK-123", 1000.0);
    Account savings = new Account("SAV-456", 5000.0);

    // Execute commands
    checking.processCommand(new PaymentCommand.Deposit(500.0));
    checking.processCommand(new PaymentCommand.Withdraw(100.0, checking.getBalance()));
    checking.processCommand(new PaymentCommand.Transfer(checking, savings, 200.0));
    checking.processCommand(new PaymentCommand.FeesApplied(2.5, "Monthly fee"));

    checking.printStatement();  // Shows all transactions

    // Undo last fee
    checking.undoLastCommand();
    System.out.println("After undo:");
    checking.printStatement();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔️ Short, type-safe, and clear.&lt;br&gt;
✔️ Commands define what happens; Account executes them.&lt;br&gt;
✔️ Undo/redo is trivial and explicit.&lt;/p&gt;

&lt;p&gt;🧭 When to Use This Modern Command Pattern&lt;br&gt;
✅ Perfect for:&lt;/p&gt;

&lt;p&gt;Undo/Redo systems&lt;br&gt;
Transaction logging&lt;br&gt;
Task queues&lt;br&gt;
Macro recording&lt;br&gt;
Workflow orchestration&lt;br&gt;
❌ Avoid for:&lt;/p&gt;

&lt;p&gt;Highly dynamic plugin systems&lt;br&gt;
Environments needing runtime type loading (sealed types limit extension)&lt;br&gt;
🏁 Wrapping Up&lt;br&gt;
The Command pattern hasn’t changed — but Java has evolved.&lt;/p&gt;

&lt;p&gt;With records, sealed interfaces, and pattern matching, you can write clean, immutable, type-safe commands with half the code and none of the pain.&lt;/p&gt;

&lt;p&gt;It’s not about replacing design patterns — it’s about making them effortless.&lt;/p&gt;

&lt;p&gt;So go ahead — modernise those command hierarchies.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>java</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
