Compile Ready
All low level design problems
Low Level Design/Concepts & Foundations/ Concept

Behavioral Patterns

Strategy, Observer, State, Command, Chain of Responsibility, and more — how objects collaborate and share responsibility.

6m readPatternsBehavioralStrategyObserverState

Introduction

Behavioral design patterns explain how objects communicate, delegate work, and change behavior without turning one service into a giant conditional block. They are common in Amazon, Google, and Uber LLD rounds because most interview systems have workflows, policies, events, queues, state transitions, and reusable algorithms.

This lesson covers Strategy, Observer, State, Command, Chain of Responsibility, Template Method, Iterator, and Mediator with Java examples, use cases, and the exact places they appear in LLD problems.

Learning Objectives

  • Explain the intent of eight core behavioral patterns in interview language.

  • Identify when a conditional should become Strategy, State, Chain of Responsibility, or Command.

  • Use Observer for event fan-out without coupling publishers to subscribers.

  • Model workflows with State while keeping invalid transitions out of service classes.

  • Use Template Method and Iterator when an algorithm skeleton or traversal contract is more important than concrete storage.

  • Recognize where these patterns naturally show up in LLD problems such as vending machines, elevators, notifications, rate limiters, and booking systems.

Core Theory

Strategy: swap an algorithm behind a stable contract

Intent: Define a family of algorithms, put each one behind the same interface, and let the caller choose or inject the behavior at runtime.

Use case: Pricing, routing, matching, discounting, allocation, retry policy, and rate limit policy are classic strategies. The owning service should not contain a long if else ladder for every policy.

Where it shows up in LLD problems: Parking lot fee calculation, cab booking driver matching, food delivery assignment, shopping cart discounts, payment gateway routing, and notification channel selection.

interface PricingStrategy {
    long priceInCents(Order order);
}

final class FlatPricingStrategy implements PricingStrategy {
    @Override
    public long priceInCents(Order order) {
        return 499;
    }
}

final class SurgePricingStrategy implements PricingStrategy {
    private final int multiplier;

    SurgePricingStrategy(int multiplier) {
        this.multiplier = multiplier;
    }

    @Override
    public long priceInCents(Order order) {
        return order.basePriceInCents() * multiplier;
    }
}

final class CheckoutService {
    private final PricingStrategy pricingStrategy;

    CheckoutService(PricingStrategy pricingStrategy) {
        this.pricingStrategy = pricingStrategy;
    }

    long quote(Order order) {
        return pricingStrategy.priceInCents(order);
    }
}

Observer: notify many subscribers about a domain event

Intent: Let a subject publish changes to many observers without knowing their concrete classes. The subject owns the event; observers own their reactions.

Use case: A booking service can publish BookingConfirmed once, while email, push notification, analytics, invoice generation, and loyalty points react independently.

Where it shows up in LLD problems: Notification systems, inventory alerts, splitwise expense updates, movie booking seat updates, text editor change listeners, and distributed cache invalidation hooks.

interface OrderObserver {
    void onOrderPlaced(Order order);
}

final class EmailNotifier implements OrderObserver {
    @Override
    public void onOrderPlaced(Order order) {
        System.out.println("Email sent for order " + order.id());
    }
}

final class OrderSubject {
    private final java.util.List<OrderObserver> observers = new java.util.ArrayList<>();

    void addObserver(OrderObserver observer) {
        observers.add(observer);
    }

    void place(Order order) {
        order.markPlaced();
        for (OrderObserver observer : observers) {
            observer.onOrderPlaced(order);
        }
    }
}

State: move state-specific behavior out of the context

Intent: Allow an object to change behavior when its internal state changes, while each state class controls the transitions it supports.

Use case: A vending machine should behave differently when idle, accepting money, dispensing, sold out, or refunding. A single method with every possible transition quickly becomes fragile.

Where it shows up in LLD problems: Vending machine, elevator system, ATM, order lifecycle, payment lifecycle, cab trip lifecycle, task scheduler job states, and document collaboration modes.

interface OrderState {
    void pay(OrderContext context);
    void ship(OrderContext context);
    void cancel(OrderContext context);
}

final class CreatedState implements OrderState {
    @Override
    public void pay(OrderContext context) {
        context.setState(new PaidState());
    }

    @Override
    public void ship(OrderContext context) {
        throw new IllegalStateException("pay before shipping");
    }

    @Override
    public void cancel(OrderContext context) {
        context.setState(new CancelledState());
    }
}

final class PaidState implements OrderState {
    @Override
    public void pay(OrderContext context) {
        throw new IllegalStateException("already paid");
    }

    @Override
    public void ship(OrderContext context) {
        context.setState(new ShippedState());
    }

    @Override
    public void cancel(OrderContext context) {
        context.setState(new CancelledState());
    }
}

final class OrderContext {
    private OrderState state = new CreatedState();

    void setState(OrderState state) {
        this.state = state;
    }

    void pay() {
        state.pay(this);
    }

    void ship() {
        state.ship(this);
    }
}

Command: turn a request into an object

Intent: Encapsulate an action, its inputs, and sometimes undo behavior in an object so it can be queued, logged, retried, scheduled, or replayed.

Use case: A text editor can store commands such as insert text, delete text, and format block in a history stack. The invoker does not need to know how each action mutates the document.

Where it shows up in LLD problems: Text editor undo and redo, task scheduler jobs, message queue consumers, remote control buttons, workflow engines, payment retries, and audit logging.

interface Command {
    void execute();
    void undo();
}

final class AddItemCommand implements Command {
    private final Cart cart;
    private final Product product;

    AddItemCommand(Cart cart, Product product) {
        this.cart = cart;
        this.product = product;
    }

    @Override
    public void execute() {
        cart.add(product);
    }

    @Override
    public void undo() {
        cart.remove(product);
    }
}

final class CommandHistory {
    private final java.util.Deque<Command> history = new java.util.ArrayDeque<>();

    void run(Command command) {
        command.execute();
        history.push(command);
    }

    void undoLast() {
        if (!history.isEmpty()) {
            history.pop().undo();
        }
    }
}

Chain of Responsibility: pass a request through handlers

Intent: Give multiple handlers a chance to process a request without hardwiring the sender to the receiver. Each handler either handles the request or forwards it to the next handler.

Use case: An API gateway can run authentication, authorization, rate limiting, validation, and routing as a chain. The request moves forward only if each handler approves it.

Where it shows up in LLD problems: ATM cash dispensing by denomination, logging framework severity routing, API gateway filters, support ticket escalation, coupon validation, and middleware pipelines.

abstract class RequestHandler {
    private RequestHandler next;

    RequestHandler linkWith(RequestHandler next) {
        this.next = next;
        return next;
    }

    final void handle(Request request) {
        if (canHandle(request)) {
            process(request);
            return;
        }
        if (next != null) {
            next.handle(request);
        }
    }

    protected abstract boolean canHandle(Request request);
    protected abstract void process(Request request);
}

final class HighPriorityHandler extends RequestHandler {
    @Override
    protected boolean canHandle(Request request) {
        return request.priority() == Priority.HIGH;
    }

    @Override
    protected void process(Request request) {
        System.out.println("Handled high priority request");
    }
}

Template Method: fix the algorithm skeleton and vary selected steps

Intent: Put the invariant sequence of an algorithm in a base class and let subclasses customize selected steps without changing the overall order.

Use case: A data import pipeline might always validate input, parse records, transform rows, persist results, and publish metrics. CSV and JSON importers vary only parsing and transformation.

Where it shows up in LLD problems: Payment processing flows, report generation, game turn execution, file upload processing, document export, and machine operation cycles.

abstract class PaymentFlow {
    public final Receipt pay(PaymentRequest request) {
        validate(request);
        authorize(request);
        Receipt receipt = capture(request);
        notifyCustomer(receipt);
        return receipt;
    }

    protected void validate(PaymentRequest request) {
        if (request.amountInCents() <= 0) {
            throw new IllegalArgumentException("amount must be positive");
        }
    }

    protected abstract void authorize(PaymentRequest request);
    protected abstract Receipt capture(PaymentRequest request);

    protected void notifyCustomer(Receipt receipt) {
        System.out.println("Receipt " + receipt.id() + " sent");
    }
}

final class CardPaymentFlow extends PaymentFlow {
    @Override
    protected void authorize(PaymentRequest request) {
        System.out.println("Card authorized");
    }

    @Override
    protected Receipt capture(PaymentRequest request) {
        return new Receipt("card", request.amountInCents());
    }
}

Iterator: expose traversal without exposing storage

Intent: Provide a standard way to traverse a collection while hiding whether the data is stored in an array, tree, graph, page cursor, or remote batch.

Use case: A playlist can support next song traversal without exposing its internal list. Later it can switch to a paginated store or filtered traversal without changing callers.

Where it shows up in LLD problems: File system traversal, text editor document traversal, library catalog scanning, search pagination, graph neighbors, playlist queues, and inventory iteration.

final class Playlist implements Iterable<Song> {
    private final java.util.List<Song> songs = new java.util.ArrayList<>();

    void add(Song song) {
        songs.add(song);
    }

    @Override
    public java.util.Iterator<Song> iterator() {
        return new PlaylistIterator(songs);
    }
}

final class PlaylistIterator implements java.util.Iterator<Song> {
    private final java.util.List<Song> songs;
    private int index;

    PlaylistIterator(java.util.List<Song> songs) {
        this.songs = songs;
    }

    @Override
    public boolean hasNext() {
        return index < songs.size();
    }

    @Override
    public Song next() {
        return songs.get(index++);
    }
}

Mediator: centralize complex peer collaboration

Intent: Reduce many-to-many coupling by routing collaboration through a mediator object. Peers talk to the mediator instead of directly knowing every other peer.

Use case: In a chat room, users should not maintain direct references to every other user. They send a message to the room, and the room decides who receives it.

Where it shows up in LLD problems: Chat rooms, air traffic control, elevator dispatching, cab marketplace matching, UI form coordination, collaborative document sessions, and game lobbies.

interface ChatMediator {
    void send(User sender, String message);
    void join(User user);
}

final class ChatRoom implements ChatMediator {
    private final java.util.List<User> users = new java.util.ArrayList<>();

    @Override
    public void join(User user) {
        users.add(user);
        user.setMediator(this);
    }

    @Override
    public void send(User sender, String message) {
        for (User user : users) {
            if (user != sender) {
                user.receive(sender.name(), message);
            }
        }
    }
}

final class User {
    private final String name;
    private ChatMediator mediator;

    User(String name) {
        this.name = name;
    }

    String name() {
        return name;
    }

    void setMediator(ChatMediator mediator) {
        this.mediator = mediator;
    }

    void send(String message) {
        mediator.send(this, message);
    }

    void receive(String from, String message) {
        System.out.println(from + ": " + message);
    }
}

Diagrams

State pattern for an order lifecycle

Rendering diagram…
OrderContext delegates lifecycle actions to the current OrderState. Each state owns valid transitions, so the context does not need a large conditional for every status.

Comparisons

State vs Strategy

DimensionStateStrategyDecision signal
Primary goalChange behavior as an object moves through lifecycle statesSwap one algorithm or policy for anotherUse State for lifecycle and Strategy for interchangeable policy
Who changes implementationThe context or current state changes the next stateThe caller, factory, or dependency injection chooses the strategyIf behavior changes because status changed, prefer State
Invalid operationsState classes can reject operations that are not allowed nowStrategies usually assume the operation is valid and vary how it runsIf you need transition rules, State gives clearer ownership
LLD exampleVending machine idle, has money, dispensing, sold outParking lot hourly, flat, or surge pricingAsk whether the variation is lifecycle or policy

Chain of Responsibility vs Decorator

DimensionChain of ResponsibilityDecoratorDecision signal
Primary goalFind a handler or pass a request through ordered checksAdd behavior around one component while preserving its interfaceUse Chain for routing or filters; use Decorator for layered enhancement
Request flowA handler may stop the chain or forward to the next handlerEach decorator usually calls the wrapped component and adds work before or afterIf one handler can end processing, Chain is the better fit
Object relationshipHandlers know only the next handlerDecorators wrap a concrete component of the same interfaceIf every layer represents the same component contract, Decorator is likely
LLD exampleAPI gateway authentication, authorization, rate limit, validationCoffee machine add-ons such as milk, sugar, and whipped creamAsk whether you are choosing a handler or enriching one object

Best Practices

  • Start from the responsibility that varies: algorithm, event reaction, lifecycle, request, pipeline step, traversal, or peer coordination.

  • Prefer small interfaces for Strategy, Observer, Command, Iterator, and Mediator collaborators.

  • Keep State transition rules inside state classes or a dedicated transition policy, not scattered across controllers.

  • Make Command objects immutable when they are queued, retried, logged, or stored for undo.

  • Use Chain of Responsibility only when order matters and a handler can approve, reject, transform, or forward the request.

  • Use Template Method sparingly in Java because inheritance is rigid; prefer Strategy when only one step varies independently.

  • Document whether Observer callbacks are synchronous or asynchronous because it changes failure handling and latency.

  • In interviews, name the problem pressure first, then introduce the pattern as the smallest clean seam.

Common Mistakes

  • ×

    Using Strategy and State interchangeably without explaining lifecycle transitions versus interchangeable algorithms.

  • ×

    Letting Observer callbacks mutate publisher internals, which reintroduces tight coupling and surprising side effects.

  • ×

    Creating one god mediator that contains every business rule instead of coordinating peers at the right abstraction level.

  • ×

    Building a Chain of Responsibility where every handler always runs and no handler can influence flow; that is usually a pipeline.

  • ×

    Using Template Method for unrelated classes that do not share a stable algorithm skeleton.

  • ×

    Making Command objects depend on UI classes, which prevents retry, scheduling, and server-side execution.

  • ×

    Exposing collection internals instead of returning an Iterator or immutable traversal view.

  • ×

    Adding patterns before requirements justify variation, which makes a simple LLD answer look over-engineered.

Quiz

0/8 answered

  1. 1.Which behavioral pattern is the best fit for swappable pricing algorithms?

  2. 2.A vending machine behaves differently when idle, accepting money, and dispensing. Which pattern is the strongest fit?

  3. 3.Which pattern lets a subject publish an event to email, analytics, and push notification subscribers without knowing their concrete classes?

  4. 4.Which pattern is most appropriate when actions must be queued, retried, logged, or undone?

  5. 5.Authentication, authorization, rate limiting, and validation are ordered checks in an API gateway. Which pattern is a good fit?

  6. 6.When should Template Method be preferred over Strategy?

  7. 7.Which pattern hides whether traversal uses a list, tree, page cursor, or remote batch?

  8. 8.Which pattern reduces many-to-many coupling among peer objects such as chat users, elevators, or UI controls?

Flashcards

Cheat Sheet

Strategy: Use for swappable algorithms such as pricing, routing, matching, retry, discounting, and allocation.

Observer: Use for event fan-out when a publisher should not know email, analytics, push, invoice, or cache invalidation subscribers.

State: Use when valid behavior depends on lifecycle status such as vending machine states, order states, ATM states, or elevator states.

Command: Use when a request must be stored as data for undo, redo, retry, scheduling, logging, replay, or queue processing.

Chain of Responsibility: Use for ordered handlers such as gateway filters, logger levels, ATM cash denominations, validation rules, and support escalation.

Template Method: Use when subclasses share a stable algorithm order but vary selected steps. Avoid it when independent strategies would be more flexible.

Iterator: Use when callers need traversal but should not know storage shape, cursor state, pagination, or filtering mechanics.

Mediator: Use when peers would otherwise know too much about each other, such as chat participants, UI controls, elevator cars, or game lobby members.

Interview shortcut: Identify the pressure first: policy variation suggests Strategy, event fan-out suggests Observer, lifecycle behavior suggests State, queued actions suggest Command, ordered handling suggests Chain of Responsibility, fixed algorithm skeleton suggests Template Method, traversal suggests Iterator, and peer coordination suggests Mediator.

References