Cheat SheetsLow Level DesignBehavioral Patterns

Behavioral Patterns — Cheat Sheet

Low Level Design · 10 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Behavioral Patterns
Low Level Design10 topicsQuick revision reference
1

Observer Pattern

Defines a one-to-many dependency so that when one object (Subject) changes state, all its dependents (Observers) are notified automatically.

  • Observer defines one-to-many dependency — Subject notifies all registered observers on state change.
  • The "lapsed listener" problem: failing to deregister observers causes memory leaks.
  • Spring's @EventListener is the production Observer implementation — use it over raw Observer.
  • java.util.Observable is deprecated since Java 9 — avoid it.
  • @Async on @EventListener makes the observer run on a separate thread (fire-and-forget).
  • Reactive streams (RxJava, Project Reactor) extend Observer with backpressure, error handling, and operators.
Java — Observer (Stock Ticker)
import java.util.ArrayList;
import java.util.List;

// Observer interface
public interface StockObserver {
    void onPriceChange(String symbol, double newPrice, double oldPrice);
}

// Subject interface
public interface StockSubject {
    void addObserver(StockObserver observer);
    void removeObserver(StockObserver observer);
    void notifyObservers();
}

// Concrete Subject
public class StockTicker implements StockSubject {
    private final String symbol;
    private double currentPrice;
    private final List<StockObserver> observers = new ArrayList<>();

    public StockTicker(String symbol, double initialPrice) {
        this.symbol       = symbol;
        this.currentPrice = initialPrice;
    }

    @Override public void addObserver(StockObserver o)    { observers.add(o); }
    @Override public void removeObserver(StockObserver o) { observers.remove(o); }

    public void setPrice(double newPrice) {
        double oldPrice   = this.currentPrice;
        this.currentPrice = newPrice;
        notifyObservers(oldPrice); // state changed — notify
    }

    private void notifyObservers(double oldPrice) {
        observers.forEach(o -> o.onPriceChange(symbol, currentPrice, oldPrice));
    }

    @Override public void notifyObservers() {
        observers.forEach(o -> o.onPriceChange(symbol, currentPrice, currentPrice));
    }
}

// Concrete Observers
public class AlertObserver implements StockObserver {
    private final double threshold;
    public AlertObserver(double threshold) { this.threshold = threshold; }

    @Override
    public void onPriceChange(String symbol, double newPrice, double oldPrice) {
        if (Math.abs(newPrice - oldPrice) / oldPrice > threshold) {
            System.out.printf("ALERT: %s moved by >%.0f%% → ₹%.2f%n",
                symbol, threshold * 100, newPrice);
        }
    }
}

public class PortfolioObserver implements StockObserver {
    private final int quantity;
    public PortfolioObserver(int quantity) { this.quantity = quantity; }

    @Override
    public void onPriceChange(String symbol, double newPrice, double oldPrice) {
        double pnl = (newPrice - oldPrice) * quantity;
        System.out.printf("Portfolio P&L for %s: %+.2f%n", symbol, pnl);
    }
}

// Usage
StockTicker infosys = new StockTicker("INFY", 1500.0);
infosys.addObserver(new AlertObserver(0.05));   // alert on 5% move
infosys.addObserver(new PortfolioObserver(100)); // holds 100 shares

infosys.setPrice(1600.0); // triggers both observers
infosys.setPrice(1580.0); // triggers both observers
2

Strategy Pattern

Defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime without changing the client.

  • Strategy replaces conditional branches with polymorphism — each branch becomes a class.
  • The Context delegates to the Strategy — it does not know or care which strategy is active.
  • Prefer @FunctionalInterface strategies; pass lambdas instead of creating concrete classes.
  • Java Comparator is the canonical Strategy — Comparator.comparing() composes strategies.
  • Strategy (behavioral) vs Bridge (structural): Strategy swaps algorithms; Bridge separates abstraction from implementation.
Java — Strategy (Payment Methods)
// Strategy interface
public interface PaymentStrategy {
    boolean pay(double amount);
    String methodName();
}

// Concrete Strategies
public class CreditCardStrategy implements PaymentStrategy {
    private final String cardNumber;
    private final String cvv;

    public CreditCardStrategy(String cardNumber, String cvv) {
        this.cardNumber = cardNumber;
        this.cvv = cvv;
    }

    @Override
    public boolean pay(double amount) {
        System.out.printf("Paid ₹%.2f via Credit Card ending %s%n",
            amount, cardNumber.substring(cardNumber.length() - 4));
        return true;
    }

    @Override public String methodName() { return "CREDIT_CARD"; }
}

public class UpiStrategy implements PaymentStrategy {
    private final String upiId;
    public UpiStrategy(String upiId) { this.upiId = upiId; }

    @Override
    public boolean pay(double amount) {
        System.out.printf("Paid ₹%.2f via UPI ID: %s%n", amount, upiId);
        return true;
    }

    @Override public String methodName() { return "UPI"; }
}

public class WalletStrategy implements PaymentStrategy {
    private double balance;
    public WalletStrategy(double balance) { this.balance = balance; }

    @Override
    public boolean pay(double amount) {
        if (balance < amount) {
            System.out.println("Insufficient wallet balance");
            return false;
        }
        balance -= amount;
        System.out.printf("Paid ₹%.2f via Wallet. Remaining: ₹%.2f%n", amount, balance);
        return true;
    }

    @Override public String methodName() { return "WALLET"; }
}

// Context
public class CheckoutContext {
    private PaymentStrategy strategy;

    public void setStrategy(PaymentStrategy strategy) {
        this.strategy = Objects.requireNonNull(strategy);
    }

    public boolean checkout(double amount) {
        if (strategy == null) throw new IllegalStateException("No payment strategy set");
        System.out.println("Attempting payment via: " + strategy.methodName());
        return strategy.pay(amount);
    }
}

// Usage — strategy swapped at runtime
CheckoutContext ctx = new CheckoutContext();

ctx.setStrategy(new UpiStrategy("akshay@okaxis"));
ctx.checkout(999.0);

ctx.setStrategy(new WalletStrategy(500.0));
ctx.checkout(999.0); // fails — insufficient balance

ctx.setStrategy(new CreditCardStrategy("4111111111111234", "123"));
ctx.checkout(999.0); // succeeds
3

Command Pattern

Encapsulates a request as an object, enabling parameterization, queuing, logging, and undo/redo of operations.

  • Command encapsulates a request as an object — enables queuing, logging, and undo/redo.
  • execute() performs the action; undo() reverses it. The deleted/old state must be saved in execute().
  • Invoker knows nothing about the command receiver — it only calls execute()/undo().
  • Command pattern is used in Java Swing (AbstractAction), Spring Batch (Job/Step), and transactional outbox.
  • A MacroCommand is a Composite of Commands — execute() calls execute() on each child.
Java — Command with Undo/Redo (Text Editor)
import java.util.ArrayDeque;
import java.util.Deque;

// Command interface
public interface Command {
    void execute();
    void undo();
    String description();
}

// Receiver
public class TextEditor {
    private final StringBuilder text = new StringBuilder();

    public void insertText(int position, String s) {
        text.insert(position, s);
    }

    public void deleteText(int position, int length) {
        text.delete(position, position + length);
    }

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

// Concrete Command — insert
public class InsertCommand implements Command {
    private final TextEditor editor;
    private final int position;
    private final String text;

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

    @Override public void execute() { editor.insertText(position, text); }
    @Override public void undo()    { editor.deleteText(position, text.length()); }
    @Override public String description() { return "Insert '" + text + "' at " + position; }
}

// Concrete Command — delete
public class DeleteCommand implements Command {
    private final TextEditor editor;
    private final int position;
    private final int length;
    private String deleted; // saved for undo

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

    @Override
    public void execute() {
        deleted = editor.getText().substring(position, position + length);
        editor.deleteText(position, length);
    }

    @Override public void undo() { editor.insertText(position, deleted); }
    @Override public String description() { return "Delete " + length + " chars at " + position; }
}

// Invoker — manages history
public class CommandInvoker {
    private final Deque<Command> history = new ArrayDeque<>();
    private final Deque<Command> undone  = new ArrayDeque<>();

    public void execute(Command cmd) {
        cmd.execute();
        history.push(cmd);
        undone.clear(); // new command clears redo stack
        System.out.println("Executed: " + cmd.description());
    }

    public void undo() {
        if (history.isEmpty()) { System.out.println("Nothing to undo"); return; }
        Command cmd = history.pop();
        cmd.undo();
        undone.push(cmd);
        System.out.println("Undone: " + cmd.description());
    }

    public void redo() {
        if (undone.isEmpty()) { System.out.println("Nothing to redo"); return; }
        Command cmd = undone.pop();
        cmd.execute();
        history.push(cmd);
        System.out.println("Redone: " + cmd.description());
    }
}

// Usage
TextEditor editor   = new TextEditor();
CommandInvoker ctrl = new CommandInvoker();

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

ctrl.undo();
System.out.println(editor.getText()); // Hello

ctrl.redo();
System.out.println(editor.getText()); // Hello World
4

Iterator Pattern

Provides a way to sequentially access elements of a collection without exposing its underlying representation.

  • Implementing Iterable<T> allows use in enhanced for-each and java.util.stream.StreamSupport.
  • Always throw NoSuchElementException (not null) when next() is called beyond the last element.
  • Iterator state is per-iterator — multiple iterators can traverse the same collection concurrently.
  • ConcurrentModificationException is thrown when a collection is modified during iteration; use CopyOnWriteArrayList or ListIterator.remove() instead.
  • Java Streams are lazy iterators that compose operations without materializing intermediate collections.
Java — Custom Iterator (IntRange + ReverseListIterator)
import java.util.Iterator;
import java.util.NoSuchElementException;

// Custom collection: a range of integers
public class IntRange implements Iterable<Integer> {
    private final int start;
    private final int end;   // exclusive

    public IntRange(int start, int end) {
        if (start > end) throw new IllegalArgumentException("start must be <= end");
        this.start = start;
        this.end   = end;
    }

    @Override
    public Iterator<Integer> iterator() {
        return new RangeIterator();
    }

    // Inner class iterator — has access to start/end
    private class RangeIterator implements Iterator<Integer> {
        private int current = start;

        @Override
        public boolean hasNext() { return current < end; }

        @Override
        public Integer next() {
            if (!hasNext()) throw new NoSuchElementException();
            return current++;
        }
    }
}

// Usage — works in for-each
IntRange range = new IntRange(1, 6);
for (int n : range) {
    System.out.print(n + " "); // 1 2 3 4 5
}

// Reverse iterator for a list
public class ReverseListIterator<T> implements Iterator<T> {
    private final List<T> list;
    private int index;

    public ReverseListIterator(List<T> list) {
        this.list  = list;
        this.index = list.size() - 1;
    }

    @Override public boolean hasNext() { return index >= 0; }

    @Override
    public T next() {
        if (!hasNext()) throw new NoSuchElementException();
        return list.get(index--);
    }
}

List<String> names = List.of("Alice", "Bob", "Charlie");
Iterator<String> rev = new ReverseListIterator<>(names);
while (rev.hasNext()) System.out.print(rev.next() + " "); // Charlie Bob Alice
5

Template Method Pattern

Defines the skeleton of an algorithm in a base class, deferring specific steps to subclasses without changing the algorithm's structure.

  • Template method is final — subclasses cannot reorder the algorithm steps.
  • Abstract steps are mandatory; hook methods are optional with default behavior.
  • Template Method uses inheritance (compile-time); Strategy uses composition (runtime-swappable).
  • Spring JdbcTemplate, RestTemplate, and AbstractBeanFactory use Template Method extensively.
  • The Hollywood Principle: base class calls subclass methods — inversion of control at the class level.
Java — Template Method (DataProcessor pipeline)
// Abstract class with template method
public abstract class DataProcessor {

    // Template method — final: defines the algorithm skeleton
    public final void process(String dataSource) {
        readData(dataSource);     // abstract — must implement
        validateData();           // abstract — must implement
        if (shouldTransform()) {  // hook — optional override
            transformData();
        }
        writeData();              // abstract — must implement
        onComplete();             // hook — optional override
    }

    protected abstract void readData(String source);
    protected abstract void validateData();
    protected abstract void writeData();

    // Hook — default: transform is enabled
    protected boolean shouldTransform() { return true; }

    // Hook — default: no-op
    protected void transformData() {}

    // Hook — default: no-op
    protected void onComplete() {}
}

// Concrete class: CSV to database
public class CsvToDatabaseProcessor extends DataProcessor {
    private List<String[]> rows;

    @Override
    protected void readData(String source) {
        System.out.println("Reading CSV from: " + source);
        rows = List.of(new String[]{"Alice","25"}, new String[]{"Bob","30"});
    }

    @Override
    protected void validateData() {
        rows.forEach(row -> {
            if (row.length != 2) throw new IllegalStateException("Invalid row format");
        });
        System.out.println("CSV validated: " + rows.size() + " rows");
    }

    @Override
    protected void writeData() {
        System.out.println("Writing " + rows.size() + " rows to database");
    }

    @Override
    protected void onComplete() {
        System.out.println("CSV processing complete. Sending notification.");
    }
}

// Concrete class: JSON — no transformation needed
public class JsonProcessor extends DataProcessor {
    @Override
    protected void readData(String source)  { System.out.println("Reading JSON: " + source); }
    @Override
    protected void validateData()           { System.out.println("Validating JSON schema"); }
    @Override
    protected void writeData()              { System.out.println("Indexing JSON to Elasticsearch"); }
    @Override
    protected boolean shouldTransform()     { return false; } // skip transform step
}

// Client
new CsvToDatabaseProcessor().process("students.csv");
new JsonProcessor().process("courses.json");
6

State Pattern

Allows an object to alter its behavior when its internal state changes, appearing to change its class.

  • State eliminates if-else/switch chains on status fields — each state is a class.
  • Context delegates all operations to the current State; State transitions by replacing the state object.
  • State classes can reference the Context to trigger transitions (state.dispense() calls machine.setState(...)).
  • Invalid operations in a state are handled locally (throw or silently ignore) — no scattered null checks.
  • Order lifecycle (PENDING→CONFIRMED→SHIPPED→DELIVERED) and TCP connection are real-world State machines.
Java — Vending Machine with State pattern
// State interface
public interface VendingMachineState {
    void insertCoin(VendingMachine machine, double amount);
    void selectProduct(VendingMachine machine, String product);
    void dispense(VendingMachine machine);
    void refund(VendingMachine machine);
}

// Context
public class VendingMachine {
    private VendingMachineState state;
    private double balance    = 0;
    private int    stockCount = 10;

    public VendingMachine() {
        this.state = new IdleState();
    }

    public void setState(VendingMachineState state) { this.state = state; }
    public double getBalance()                      { return balance; }
    public void setBalance(double b)                { this.balance = b; }
    public int  getStock()                          { return stockCount; }
    public void decrementStock()                    { stockCount--; }

    // Delegates all behavior to current state
    public void insertCoin(double amount)     { state.insertCoin(this, amount); }
    public void selectProduct(String product) { state.selectProduct(this, product); }
    public void dispense()                    { state.dispense(this); }
    public void refund()                      { state.refund(this); }
}

// Concrete States
public class IdleState implements VendingMachineState {
    @Override
    public void insertCoin(VendingMachine m, double amount) {
        m.setBalance(m.getBalance() + amount);
        System.out.println("Coin inserted: ₹" + amount + ". Balance: ₹" + m.getBalance());
        m.setState(new HasMoneyState()); // transition
    }
    @Override public void selectProduct(VendingMachine m, String p) { System.out.println("Please insert coin first"); }
    @Override public void dispense(VendingMachine m)                { System.out.println("Please insert coin first"); }
    @Override public void refund(VendingMachine m)                  { System.out.println("No money to refund"); }
}

public class HasMoneyState implements VendingMachineState {
    private String selectedProduct;

    @Override
    public void insertCoin(VendingMachine m, double amount) {
        m.setBalance(m.getBalance() + amount);
        System.out.println("Added ₹" + amount + ". Total: ₹" + m.getBalance());
    }

    @Override
    public void selectProduct(VendingMachine m, String product) {
        double price = 20.0; // simplified
        if (m.getBalance() >= price) {
            this.selectedProduct = product;
            System.out.println("Selected: " + product);
            m.setState(new DispensingState(product, price));
        } else {
            System.out.println("Insufficient balance. Need ₹" + price);
        }
    }

    @Override public void dispense(VendingMachine m)     { System.out.println("Please select a product first"); }

    @Override
    public void refund(VendingMachine m) {
        System.out.println("Refunding ₹" + m.getBalance());
        m.setBalance(0);
        m.setState(new IdleState());
    }
}

public class DispensingState implements VendingMachineState {
    private final String product;
    private final double price;

    public DispensingState(String product, double price) {
        this.product = product; this.price = price;
    }

    @Override
    public void dispense(VendingMachine m) {
        System.out.println("Dispensing: " + product);
        m.setBalance(m.getBalance() - price);
        m.decrementStock();
        if (m.getBalance() > 0) System.out.println("Change: ₹" + m.getBalance());
        m.setBalance(0);
        m.setState(m.getStock() > 0 ? new IdleState() : new OutOfStockState());
    }

    @Override public void insertCoin(VendingMachine m, double a) { System.out.println("Dispensing in progress"); }
    @Override public void selectProduct(VendingMachine m, String p) { System.out.println("Dispensing in progress"); }
    @Override public void refund(VendingMachine m)               { System.out.println("Cannot refund while dispensing"); }
}

public class OutOfStockState implements VendingMachineState {
    @Override public void insertCoin(VendingMachine m, double a)    { System.out.println("Out of stock — coin returned"); }
    @Override public void selectProduct(VendingMachine m, String p) { System.out.println("Out of stock"); }
    @Override public void dispense(VendingMachine m)                { System.out.println("Out of stock"); }
    @Override public void refund(VendingMachine m)                  { System.out.println("No money inserted"); }
}

// Usage
VendingMachine vm = new VendingMachine();
vm.insertCoin(20.0);
vm.selectProduct("Water");
vm.dispense();
7

Chain of Responsibility

Passes a request along a chain of handlers, each deciding to process it or forward it to the next handler.

  • CoR decouples sender from receiver — the sender does not know which handler processes the request.
  • Classic CoR stops at the first handler that processes; Filter Chain always forwards unless short-circuited.
  • Handlers can be added/removed/reordered at runtime without changing client code.
  • Spring Security Filter Chain processes authentication/authorization through a fixed sequence of filters.
  • Debugging long chains is hard — consider adding logging at each handler for traceability.
Java — Logger Chain of Responsibility
public enum LogLevel { DEBUG, INFO, WARN, ERROR }

// Abstract Handler
public abstract class Logger {
    protected final LogLevel level;
    protected Logger next;

    public Logger(LogLevel level) { this.level = level; }

    public Logger setNext(Logger next) {
        this.next = next;
        return next;
    }

    public final void log(LogLevel msgLevel, String message) {
        if (msgLevel.ordinal() >= this.level.ordinal()) {
            write(message); // this handler can process it
        }
        if (next != null) {
            next.log(msgLevel, message); // always forward (unlike classic CoR stop)
        }
    }

    protected abstract void write(String message);
}

// Concrete Handlers
public class ConsoleLogger extends Logger {
    public ConsoleLogger(LogLevel level) { super(level); }
    @Override protected void write(String msg) {
        System.out.println("[CONSOLE] " + msg);
    }
}

public class FileLogger extends Logger {
    public FileLogger(LogLevel level) { super(level); }
    @Override protected void write(String msg) {
        System.out.println("[FILE]    " + msg); // writes to rotating log file
    }
}

public class AlertLogger extends Logger {
    public AlertLogger(LogLevel level) { super(level); }
    @Override protected void write(String msg) {
        System.out.println("[ALERT]   " + msg); // sends PagerDuty alert
    }
}

// Build chain: Console handles DEBUG+, File handles WARN+, Alert handles ERROR+
Logger chain = new ConsoleLogger(LogLevel.DEBUG);
chain.setNext(new FileLogger(LogLevel.WARN))
     .setNext(new AlertLogger(LogLevel.ERROR));

chain.log(LogLevel.DEBUG, "Starting application");  // Console only
chain.log(LogLevel.WARN,  "High memory usage");     // Console + File
chain.log(LogLevel.ERROR, "Database unreachable");  // Console + File + Alert
8

Mediator Pattern

Defines an object that encapsulates how a set of objects interact, promoting loose coupling by preventing direct references between them.

  • Mediator reduces O(N²) peer-to-peer references to O(N) hub-and-spoke references.
  • Colleagues hold only a reference to the Mediator, never to each other.
  • The Mediator can become a "god object" anti-pattern if it takes on too much logic — keep it thin.
  • MediatR (C#) and Spring's ApplicationEventPublisher are Mediator implementations.
  • Difference from Facade: Facade simplifies a subsystem for external clients; Mediator coordinates objects within a subsystem.
Java — ChatRoom Mediator
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

// Mediator interface
public interface ChatMediator {
    void register(User user);
    void sendMessage(String message, String fromUserId, String toUserId); // DM
    void broadcast(String message, String fromUserId);                     // group
}

// Colleague
public class User {
    private final String id;
    private final String name;
    private final ChatMediator mediator;

    public User(String id, String name, ChatMediator mediator) {
        this.id       = id;
        this.name     = name;
        this.mediator = mediator;
        mediator.register(this); // self-register
    }

    public String getId()   { return id; }
    public String getName() { return name; }

    public void send(String message, String toUserId) {
        mediator.sendMessage(message, this.id, toUserId);
    }

    public void broadcast(String message) {
        mediator.broadcast(message, this.id);
    }

    // Called by mediator when a message is delivered here
    public void receive(String message, String fromName) {
        System.out.printf("[%s] ← %s: %s%n", this.name, fromName, message);
    }
}

// Concrete Mediator — ChatRoom
public class ChatRoom implements ChatMediator {
    private final Map<String, User> users = new ConcurrentHashMap<>();

    @Override
    public void register(User user) {
        users.put(user.getId(), user);
        System.out.println(user.getName() + " joined the chat");
    }

    @Override
    public void sendMessage(String message, String fromId, String toId) {
        User sender    = users.get(fromId);
        User recipient = users.get(toId);
        if (sender == null || recipient == null) return;
        recipient.receive(message, sender.getName()); // mediator routes
    }

    @Override
    public void broadcast(String message, String fromId) {
        User sender = users.get(fromId);
        if (sender == null) return;
        users.values().stream()
             .filter(u -> !u.getId().equals(fromId))
             .forEach(u -> u.receive(message, sender.getName()));
    }
}

// Usage — users never hold references to each other
ChatMediator room = new ChatRoom();
User alice = new User("u1", "Alice", room);
User bob   = new User("u2", "Bob",   room);
User carol = new User("u3", "Carol", room);

alice.send("Hey Bob, saw the LLD course?", "u2");  // DM to Bob
bob.broadcast("Everyone check aicancode.org!");   // to Alice + Carol
9

Memento Pattern

Captures and externalizes an object's internal state so it can be restored later, without violating encapsulation.

  • Memento preserves encapsulation — the Caretaker stores but never reads the Memento's state.
  • Use a private inner Memento class inside the Originator to restrict access to state fields.
  • Memory cost: each Memento stores a full state snapshot — use incremental/delta mementos for large objects.
  • Java serialization is an alternative Memento implementation for complex object graphs.
  • Undo history = stack of Mementos (push on save, pop on undo).
Java — Game save/undo with Memento
import java.util.ArrayDeque;
import java.util.Deque;

// Originator — the object whose state we want to save/restore
public class GameCharacter {
    private String name;
    private int    health;
    private int    level;
    private String location;

    public GameCharacter(String name) {
        this.name     = name;
        this.health   = 100;
        this.level    = 1;
        this.location = "start";
    }

    // Create a snapshot (Memento)
    public Memento save() {
        return new Memento(health, level, location);
    }

    // Restore from a snapshot
    public void restore(Memento memento) {
        this.health   = memento.health;
        this.level    = memento.level;
        this.location = memento.location;
        System.out.println(name + " restored to: " + this);
    }

    public void takeDamage(int dmg)   { health = Math.max(0, health - dmg); }
    public void gainLevel()           { level++; health = 100; }
    public void moveTo(String loc)    { location = loc; }

    @Override
    public String toString() {
        return String.format("HP=%d, Level=%d, Loc=%s", health, level, location);
    }

    // Memento — inner class has access to private state
    // Caretaker only sees the opaque Memento type, not its fields
    public static final class Memento {
        private final int    health;   // private — Caretaker cannot read
        private final int    level;
        private final String location;

        private Memento(int health, int level, String location) {
            this.health   = health;
            this.level    = level;
            this.location = location;
        }
    }
}

// Caretaker — manages history, never inspects Memento internals
public class GameSaveManager {
    private final Deque<GameCharacter.Memento> history = new ArrayDeque<>();

    public void save(GameCharacter character) {
        history.push(character.save());
        System.out.println("Game saved (" + history.size() + " saves)");
    }

    public void undo(GameCharacter character) {
        if (history.isEmpty()) { System.out.println("No saves to restore"); return; }
        character.restore(history.pop());
    }
}

// Usage
GameCharacter hero = new GameCharacter("Akshay");
GameSaveManager saves = new GameSaveManager();

System.out.println("Initial: " + hero);
saves.save(hero);               // save: HP=100, L=1

hero.moveTo("dungeon");
hero.takeDamage(60);
System.out.println("After fight: " + hero); // HP=40, L=1, dungeon

saves.save(hero);               // save: HP=40, L=1, dungeon
hero.gainLevel();
hero.moveTo("castle");
System.out.println("After level-up: " + hero); // HP=100, L=2, castle

saves.undo(hero);               // restore: HP=40, L=1, dungeon
saves.undo(hero);               // restore: HP=100, L=1, start
10

Visitor Pattern

Lets you add new operations to an object structure without modifying the classes, using double dispatch.

  • Double dispatch: accept(visitor) calls visitor.visit(this) — both element type and visitor type are resolved at runtime.
  • Adding a new operation = new Visitor class (OCP). Adding a new element = update all Visitors (OCP violation).
  • Best for stable element hierarchies (e.g. AST nodes) with frequently added operations.
  • Java's instanceof pattern matching (Java 16+) and sealed classes can replace Visitor in some cases.
  • Visitor breaks encapsulation slightly — Visitor methods need access to element internals.
Java — Visitor on Expression AST
// Visitor interface — one method per element type
public interface ExpressionVisitor<T> {
    T visitNumber(NumberExpr expr);
    T visitAdd(AddExpr expr);
    T visitMultiply(MultiplyExpr expr);
}

// Element interface
public interface Expression {
    <T> T accept(ExpressionVisitor<T> visitor);
}

// Concrete Elements
public class NumberExpr implements Expression {
    public final double value;
    public NumberExpr(double value) { this.value = value; }

    @Override
    public <T> T accept(ExpressionVisitor<T> visitor) {
        return visitor.visitNumber(this); // double dispatch
    }
}

public class AddExpr implements Expression {
    public final Expression left, right;
    public AddExpr(Expression left, Expression right) {
        this.left = left; this.right = right;
    }

    @Override
    public <T> T accept(ExpressionVisitor<T> visitor) {
        return visitor.visitAdd(this);
    }
}

public class MultiplyExpr implements Expression {
    public final Expression left, right;
    public MultiplyExpr(Expression left, Expression right) {
        this.left = left; this.right = right;
    }

    @Override
    public <T> T accept(ExpressionVisitor<T> visitor) {
        return visitor.visitMultiply(this);
    }
}

// Visitor 1: Evaluate
public class EvaluateVisitor implements ExpressionVisitor<Double> {
    @Override public Double visitNumber(NumberExpr e)   { return e.value; }
    @Override public Double visitAdd(AddExpr e)         { return e.left.accept(this) + e.right.accept(this); }
    @Override public Double visitMultiply(MultiplyExpr e) { return e.left.accept(this) * e.right.accept(this); }
}

// Visitor 2: Pretty Print
public class PrintVisitor implements ExpressionVisitor<String> {
    @Override public String visitNumber(NumberExpr e)     { return String.valueOf(e.value); }
    @Override public String visitAdd(AddExpr e)           { return "(" + e.left.accept(this) + " + " + e.right.accept(this) + ")"; }
    @Override public String visitMultiply(MultiplyExpr e) { return "(" + e.left.accept(this) + " * " + e.right.accept(this) + ")"; }
}

// Usage — (3 + 4) * 2
Expression ast = new MultiplyExpr(
    new AddExpr(new NumberExpr(3), new NumberExpr(4)),
    new NumberExpr(2));

System.out.println(ast.accept(new PrintVisitor()));    // ((3.0 + 4.0) * 2.0)
System.out.println(ast.accept(new EvaluateVisitor())); // 14.0
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/lld