Behavioral Patterns
Strategy, Observer, State, Command, Chain of Responsibility, and more — how objects collaborate and share responsibility.
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
Comparisons
State vs Strategy
| Dimension | State | Strategy | Decision signal |
|---|---|---|---|
| Primary goal | Change behavior as an object moves through lifecycle states | Swap one algorithm or policy for another | Use State for lifecycle and Strategy for interchangeable policy |
| Who changes implementation | The context or current state changes the next state | The caller, factory, or dependency injection chooses the strategy | If behavior changes because status changed, prefer State |
| Invalid operations | State classes can reject operations that are not allowed now | Strategies usually assume the operation is valid and vary how it runs | If you need transition rules, State gives clearer ownership |
| LLD example | Vending machine idle, has money, dispensing, sold out | Parking lot hourly, flat, or surge pricing | Ask whether the variation is lifecycle or policy |
Chain of Responsibility vs Decorator
| Dimension | Chain of Responsibility | Decorator | Decision signal |
|---|---|---|---|
| Primary goal | Find a handler or pass a request through ordered checks | Add behavior around one component while preserving its interface | Use Chain for routing or filters; use Decorator for layered enhancement |
| Request flow | A handler may stop the chain or forward to the next handler | Each decorator usually calls the wrapped component and adds work before or after | If one handler can end processing, Chain is the better fit |
| Object relationship | Handlers know only the next handler | Decorators wrap a concrete component of the same interface | If every layer represents the same component contract, Decorator is likely |
| LLD example | API gateway authentication, authorization, rate limit, validation | Coffee machine add-ons such as milk, sugar, and whipped cream | Ask 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.Which behavioral pattern is the best fit for swappable pricing algorithms?
2.A vending machine behaves differently when idle, accepting money, and dispensing. Which pattern is the strongest fit?
3.Which pattern lets a subject publish an event to email, analytics, and push notification subscribers without knowing their concrete classes?
4.Which pattern is most appropriate when actions must be queued, retried, logged, or undone?
5.Authentication, authorization, rate limiting, and validation are ordered checks in an API gateway. Which pattern is a good fit?
6.When should Template Method be preferred over Strategy?
7.Which pattern hides whether traversal uses a list, tree, page cursor, or remote batch?
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
- BookDesign Patterns: Elements of Reusable Object-Oriented Software — Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides
- BookHead First Design Patterns — Eric Freeman, Elisabeth Robson, Bert Bates, and Kathy Sierra
- BlogRefactoring.Guru Behavioral Design Patterns
- DocsOracle Java Tutorials: Interfaces