Skip to content

05. Behavioral patterns — wire actions so the building reacts cleanly

~17 min read. Classes may look neat, but behavior still becomes spaghetti fast.

Built on the ELI5 in 00-eli5.md. The wiring — behavioral logic, state machines, event handlers — decides what happens when the switch flips.


1) Why behavioral patterns matter

See. Most production bugs are not about missing classes. They are about messy decisions and reactions. One event triggers five handlers in random order. One object behaves differently in each state. One algorithm changes by customer type.

Behavioral patterns organize these moving parts. They answer questions like these.

  • How do I swap one algorithm for another?
  • How do many listeners react to one event?
  • How do I capture a request as an object?
  • How do I model state-dependent behavior cleanly?
  • How do I keep one algorithm skeleton stable?

A quick map helps.

Choose one algorithm at runtime        → Strategy
Notify many listeners about an event   → Observer
Wrap an action as an object            → Command
Change behavior by current state       → State
Fix the algorithm skeleton, vary steps → Template Method

That is the floor plan for behavior. The electrical board has labeled switches. No random loose wires.

2) Strategy and Observer

Strategy is for interchangeable algorithms. You keep one contract, then plug different implementations. Good for pricing, ranking, sorting, fraud rules.

interface DeliveryFeeStrategy {
    int fee(int distanceKm, int orderValue);
}

class StandardFeeStrategy implements DeliveryFeeStrategy { ... }
class FestivalFeeStrategy implements DeliveryFeeStrategy { ... }
class PremiumUserFeeStrategy implements DeliveryFeeStrategy { ... }

Caller uses the interface. The chosen algorithm becomes a configuration or runtime choice. That keeps the wiring replaceable.

Worked example with numbers. Standard fee: 10 + 5 * km. Festival fee: 20 + 7 * km. For 4 km, standard gives 30. Festival gives 48. Same input. Different strategy. No giant conditional ladder required.

Observer is about one event, many reactions. Publisher knows subscribers through a light contract. Subscribers come and go independently.

interface OrderListener {
    void onOrderPlaced(Order order);
}

class OrderPublisher {
    private final List<OrderListener> listeners;

    void publish(Order order) {
        for (OrderListener listener : listeners) {
            listener.onOrderPlaced(order);
        }
    }
}

Listeners could be inventory updater, notification sender, analytics tracker, and fraud scorer. One event, many reactions. Simple, no?

But caution. Observer can become hidden coupling if event flow is unclear. Document listeners. Monitor failures. Keep retries deliberate. The plumbing must stay visible.

3) Command and State

Command wraps an action as an object. Useful when you want queuing, retries, undo, scheduling, or uniform handling of many requests.

interface Command {
    void execute();
}

class CancelOrderCommand implements Command {
    private final OrderService orderService;
    private final String orderId;

    public void execute() {
        orderService.cancel(orderId);
    }
}

Now command objects can sit in a queue. They can be logged, retried, or delayed. That is very handy in job systems.

Worked number example. Suppose 1,000 refund requests arrive in one minute. If each refund command takes 50 ms, a 10-worker queue can finish roughly 200 per second. Without command objects, retry metadata is scattered. With commands, each job carries its own intent.

State pattern is different. Behavior changes based on current state, but you do not want giant switch blocks everywhere.

Example: order lifecycle. Placed, Packed, Shipped, Delivered, Cancelled. Each state allows different actions.

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

class PlacedState implements OrderState { ... }
class ShippedState implements OrderState { ... }

Now cancel() in PlacedState may succeed. cancel() in ShippedState may reject. Behavior lives with the state, not one giant if-else. That keeps wiring organized.

Quick diagram:

Placed ──ship──→ Shipped ──deliver──→ Delivered
   └──cancel──→ Cancelled

4) Template Method and pattern selection

Template Method fixes the skeleton of an algorithm. Subclasses override certain steps. Useful when flow stays same, but one step varies.

Example: report generation. Every report does these steps. Fetch data. Validate data. Format output. Publish file. Only formatting differs by report type.

abstract class ReportGenerator {
    final void generate() {
        fetchData();
        validate();
        format();
        publish();
    }

    abstract void format();
}

CSV report and PDF report can override format(). Skeleton stays safe in the base class. Caller gets predictable flow.

Now compare the patterns quickly.

  • Strategy: swap whole algorithm.
  • Observer: fan out one event.
  • Command: treat a request as data.
  • State: move behavior into state objects.
  • Template Method: freeze algorithm order, vary steps.

Selection question helps. If you say, "I need different pricing formulas," choose Strategy. If you say, "Order placed should trigger many independent actions," choose Observer. If you say, "I need retryable jobs," choose Command. If you say, "Behavior changes after shipment," choose State. If you say, "Flow is fixed but formatting differs," choose Template Method.

5) One realistic order flow

Let us combine them in one small story. No drama. Just design.

Customer clicks cancel
CancelOrderCommand
        │ execute
OrderContext with current state
        ├── if PlacedState   → cancel allowed
        └── if ShippedState  → cancel denied

OrderCancelled event
        ├── RefundListener
        ├── InventoryListener
        └── NotificationListener

See how the pieces cooperate. Command captures the request. State decides legal behavior. Observer fans out consequences. This is clean wiring. Not accidental complexity.

One more rule. Do not combine five patterns for one tiny feature. Use the lightest tool that makes change safe. Pattern fit beats pattern count. Always.


Where this lives in the wild

  • At Flipkart, a backend engineer uses strategy objects for seller-specific pricing and shipping rules.
  • At Stripe, a payments engineer models payment intent states so allowed actions depend on current lifecycle stage.
  • At GitHub, a platform engineer uses command-style background jobs for retries, scheduling, and auditability.
  • At BigBasket, an order platform engineer publishes order events to inventory, notification, and refund listeners.
  • At Canva, an export pipeline engineer uses template methods for report and asset generation flows with fixed stages.

Pause and recall

  1. When is Strategy a better fit than Template Method?
  2. Why can Observer become dangerous if event flows are hidden?
  3. How does Command help with retries and queues?
  4. Which pattern makes state-dependent behavior explicit and local?

Interview Q&A

Why Strategy not one switch inside service?

Because algorithm variation grows over time and deserves isolation. Strategies reduce branching hotspots and enable testing per formula. Common wrong answer to avoid: "Strategy is only useful when there are many classes already."

Why Observer not direct method calls to every dependent service?

Because publishers should not own every downstream dependency explicitly. Observers decouple event fanout and lifecycle better. Common wrong answer to avoid: "Observer means fire-and-forget, so failures do not matter."

Why Command not plain service method calls for async work?

Because commands package intent, arguments, and execution boundary together. That makes queuing and retries much cleaner. Common wrong answer to avoid: "A command is just a DTO with a fancy name."

Why State not one enum plus many if checks?

Because state-specific behavior spreads badly with conditionals. State objects localize rules per lifecycle stage. Common wrong answer to avoid: "Enum checks are always simpler and therefore better."


Apply now (5 min)

Exercise: Design a Subscription lifecycle with Trial, Active, Paused, and Cancelled states. Choose one behavioral pattern for billing algorithm changes, and another for sending renewal notifications. Write one line per pattern choice.

Sketch from memory: Redraw the order flow diagram with Command, State, and Observer. Then add one strategy-based fee calculation beside it. Label each box clearly.


Bridge. After behavior is clear, we can draw better class diagrams and schemas with confidence. → 06-class-and-schema-design.md