Behavioral Patterns: Observer, Command, Strategy, Template Method & Chain of Responsibility
Behavioral design patterns define how objects communicate and delegate work. Master Observer (event notification), Command (encapsulate requests), Strategy (interchangeable algorithms), Template Method (algorithm skeleton with extension points), and Chain of Responsibility (pipeline processing) — with Java examples and real interview applications.
Behavioral Patterns: How Objects Communicate
Behavioral patterns address how objects interact and distribute responsibilities. While structural patterns focus on what objects are made of, behavioral patterns focus on how they talk to each other. The recurring challenge they solve is: how do you decouple the sender of a request from the receiver, so either side can change without affecting the other?
The key insight shared across behavioral patterns is delegation: the caller doesn't know the specific handler, just an interface. This enables runtime flexibility (swap strategies, add observers, change the processing chain) without modifying the caller's code.
What Earns Each Level
6/10: Names patterns. Provides textbook definitions. Implements a working but rigid example.
8/10: Selects the right pattern based on the problem description. Implements cleanly with correct abstraction level. Explains what the pattern decouples. Knows when NOT to use the pattern.
10/10: Chains patterns together (Command + Observer for a UI event system). Connects to real frameworks: Observer = Spring ApplicationEvent / RxJava Observable / GUI listeners; Strategy = Java Comparator; Template Method = Spring's JdbcTemplate; Chain of Responsibility = HTTP middleware, Java servlet filters. Identifies when adding a behavioral pattern increases complexity more than it reduces coupling.
The Five Core Behavioral Patterns
Observer — Notify multiple dependents of state changes
When one object changes state, all dependent objects are notified automatically. Subject maintains a list of Observers and calls update() on each when it changes. Observer registers/unregisters with the Subject. Decouples Subject from its dependents — Subject doesn't know how many or what type of Observers exist. Signal: 'When X changes, multiple independent components should react.' Examples: event system, stock ticker, UI data binding, notification service. Java: EventListener, RxJava Observable, Spring ApplicationEvent.
Command — Encapsulate a request as an object
Package a request (method call + parameters) into an object with an execute() method. Benefits: decouple sender from receiver, queue/log/retry commands, support undo (store a reverse command). Signal: 'We need to queue operations, support undo, or log all actions.' Examples: undo/redo in a text editor, task queue, remote procedure, macro recording. The Command object knows the receiver; the invoker knows only the Command interface.
Strategy — Make algorithms interchangeable
Define a family of algorithms, encapsulate each, and make them interchangeable. Client selects the strategy at runtime or construction time. Eliminates large conditional logic (switch on sort type → inject the right sorter). Signal: 'There are multiple ways to do X, and we want to switch between them.' Examples: sorting algorithms, payment methods, routing algorithms, pricing strategies, compression methods. Java: Comparator is the canonical Strategy example.
Template Method — Define the algorithm skeleton, let subclasses fill in steps
Abstract class defines the steps of an algorithm. Some steps have default implementations; others are abstract (left to subclasses). The overall structure (the template method) is fixed; specific steps vary. Provides the 'Hollywood Principle': don't call us, we'll call you — the base class calls the subclass's overridden steps. Signal: 'All variants of this process share the same overall steps, but differ in specific sub-steps.' Examples: data parsers (read → parse → validate → save), Spring's JdbcTemplate, test setUp/tearDown.
Chain of Responsibility — Pass request through a chain of handlers
Each handler decides whether to process the request or pass it to the next handler in the chain. Decouples sender from concrete handler; handlers can be reordered, added, or removed at runtime. Signal: 'Multiple objects may handle a request; we want to avoid hard-coding which one.' Examples: HTTP middleware (auth → rate-limit → log → handle), exception handling (try-catch chain), customer support escalation (L1 → L2 → L3). Each handler has a reference to the next handler.
Observer Pattern: Event Notification System
Implementation Templates
Observer:
interface Observer { void update(Event event); }
class EventBus {
private final Map<String, List<Observer>> listeners = new HashMap<>();
public void subscribe(String event, Observer observer) {
listeners.computeIfAbsent(event, k -> new ArrayList<>()).add(observer);
}
public void publish(String event, Event data) {
listeners.getOrDefault(event, List.of()).forEach(o -> o.update(data));
}
}
// Usage: eventBus.publish("ORDER_PLACED", orderEvent);
Strategy — Payment processing:
interface PaymentStrategy { void pay(double amount); }
class CreditCardPayment implements PaymentStrategy {
public void pay(double amount) { /* charge credit card */ }
}
class CryptoPayment implements PaymentStrategy {
public void pay(double amount) { /* broadcast transaction */ }
}
class Checkout {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy s) { this.strategy = s; }
public void checkout(double total) { strategy.pay(total); } // no if-else!
}
Command — Text Editor with Undo:
interface Command { void execute(); void undo(); }
class InsertTextCommand implements Command {
private final Document doc;
private final String text;
private final int position;
public void execute() { doc.insert(position, text); }
public void undo() { doc.delete(position, text.length()); }
}
class CommandHistory {
private final Deque<Command> history = new ArrayDeque<>();
public void execute(Command cmd) { cmd.execute(); history.push(cmd); }
public void undo() { if (!history.isEmpty()) history.pop().undo(); }
}
Chain of Responsibility — HTTP Middleware:
abstract class Middleware {
private Middleware next;
public Middleware setNext(Middleware next) { this.next = next; return next; }
public abstract boolean handle(HttpRequest req, HttpResponse resp);
protected boolean passToNext(HttpRequest req, HttpResponse resp) {
if (next == null) return true; // end of chain: request handled
return next.handle(req, resp);
}
}
class AuthMiddleware extends Middleware {
public boolean handle(HttpRequest req, HttpResponse resp) {
if (!req.hasValidToken()) { resp.setStatus(401); return false; }
return passToNext(req, resp); // pass to next handler
}
}
class RateLimitMiddleware extends Middleware {
public boolean handle(HttpRequest req, HttpResponse resp) {
if (isRateLimited(req.getIp())) { resp.setStatus(429); return false; }
return passToNext(req, resp);
}
}
// Chain: auth.setNext(rateLimit).setNext(logger).setNext(handler);
Template Method — Data Parser:
abstract class DataParser {
// Template method — defines the algorithm skeleton
public final void parse(String source) {
String raw = read(source); // step 1: always the same
Object data = parse(raw); // step 2: varies by format
validate(data); // step 3: may vary
save(data); // step 4: always the same
}
protected abstract String read(String source); // must override
protected abstract Object parse(String raw); // must override
protected void validate(Object data) {} // optional hook
private void save(Object data) { db.save(data); } // fixed
}
class JsonParser extends DataParser {
protected String read(String source) { return Files.readString(Path.of(source)); }
protected Object parse(String raw) { return JSON.parse(raw); }
}
Pattern Selection Guide
| Problem signal | Pattern | What it decouples |
|---|---|---|
| 'Multiple components need to react when X changes' | Observer | Subject from its dependents |
| 'Need to queue, undo, or log operations' | Command | Sender from receiver; request from execution |
| 'Multiple algorithms for the same task; want to switch at runtime' | Strategy | Algorithm from the context that uses it |
| 'Same overall process, different specific steps' | Template Method | Algorithm skeleton from step implementations |
| 'Multiple handlers may process a request; want to add/remove handlers' | Chain of Responsibility | Sender from concrete handler; handlers from each other |
| 'Need to notify exactly one subscriber (not all)' | Observer variant or direct callback | Observer notifies ALL subscribers — for one-to-one, use a callback/handler directly |
Common Pitfalls
Observer — memory leaks. If an Observer registers with a Subject but forgets to unsubscribe, the Subject holds a reference to the Observer forever — preventing garbage collection. In Java: use WeakReference for Observer storage, or ensure unsubscribe() is called in cleanup/destructor. This is a classic Android memory leak.
Observer — notification order. Don't assume Observers are notified in registration order. If Order matters, it's not Observer — use a sorted list or explicit sequencing.
Strategy — state in strategy. If a Strategy object holds mutable state, it can't be safely shared across contexts. Either make strategies stateless (can be singletons/shared), or create a new instance per context.
Template Method — inflexible hierarchy. Template Method uses inheritance. If the algorithm skeleton needs to change, the base class changes — affecting all subclasses. Prefer Strategy (composition) when the skeleton itself may vary. Rule: "Template Method when the skeleton is stable; Strategy when the algorithm as a whole may swap."
Command — CQRS confusion. In CQRS (Command Query Responsibility Segregation), "Command" means a write operation. The Command design pattern means encapsulating a request. These are different uses of the word. Don't confuse them in an interview.
Interview Delivery Summary
Observer pattern signal: "When X changes, multiple things happen." Chain of Responsibility signal: "A request passes through a pipeline of handlers — each can handle or pass it on."
Strategy vs Template Method: Strategy uses composition (inject the algorithm); Template Method uses inheritance (override the step). Prefer Strategy for runtime flexibility. Use Template Method when the skeleton is fixed and variations are in specific named steps.
Command is the gateway to undo/redo, command queuing, and audit logging. Always mention all three benefits when you propose Command — interviewers want to know you're not just implementing execute(), but thinking about undo(), logging, and queue.
Real-world links earn points: Observer = Java's EventListener / RxJava / React's useEffect dependency tracking. Strategy = Java's Comparator. Chain of Responsibility = Express.js middleware / Servlet filters.