What behavioural patterns solve
Behavioural patterns are about how objects divide responsibility and communicate. Creational patterns decide how objects are made; structural patterns decide how they are connected; behavioural patterns decide who does what, and who tells whom.
They matter more than any other family in LLD interviews, because almost every classic problem is built around one or two of them:
- A parking lot charges differently by vehicle type and time → Strategy.
- A vending machine behaves differently when idle, holding money or dispensing → State.
- Users should be notified when a booked show is cancelled → Observer.
- A text editor needs undo → Command and Memento.
- An ATM dispenses ₹2000, ₹500 and ₹100 notes in turn → Chain of Responsibility.
Interviewers probe two things: whether you can implement the pattern correctly, and — more importantly — whether you recognise which problem calls for it. This lesson covers each pattern with the problem, a text structure diagram, runnable Java, when to use it, when not to, and a typical interview question. It ends with a table that maps each pattern to the LLD problems where it appears, which you will use throughout the case studies starting with the parking lot.
Strategy
Problem
A class needs one of several interchangeable algorithms — pricing rules, sorting orders, routing choices, discount schemes — and the choice may change by configuration or at runtime. Writing them as if-else branches inside the class means every new algorithm edits tested code and the class keeps growing.
Strategy puts each algorithm in its own class behind a common interface. The context (the class that needs the algorithm) holds a reference to a strategy and delegates to it.
Structure
+-------------------+ uses +---------------------------+
| ParkingFeeCalc |-------->| <<interface>> FeeStrategy |
| (context) | +---------------------------+
+-------------------+ | + fee(minutes): long |
+-------------^-------------+
:
+---------------------------+---------+
: : :
+---------------+ +----------------+ +-------------+
| HourlyFee | | FlatDailyFee | | WeekendFee |
+---------------+ +----------------+ +-------------+
Java code
Parking fees: hourly with rounding up, or a flat fee capped per day.
interface FeeStrategy {
long feePaise(long minutesParked);
}
class HourlyFee implements FeeStrategy {
private final long perHourPaise;
HourlyFee(long perHourPaise) { this.perHourPaise = perHourPaise; }
public long feePaise(long minutes) {
long hours = Math.max(1, (minutes + 59) / 60); // round up, minimum 1 hour
return hours * perHourPaise;
}
}
class CappedDailyFee implements FeeStrategy {
private final long perHourPaise;
private final long dailyCapPaise;
CappedDailyFee(long perHourPaise, long dailyCapPaise) {
this.perHourPaise = perHourPaise;
this.dailyCapPaise = dailyCapPaise;
}
public long feePaise(long minutes) {
long fullDays = minutes / (24 * 60);
long rest = minutes % (24 * 60);
long restHours = (rest + 59) / 60;
return fullDays * dailyCapPaise + Math.min(dailyCapPaise, restHours * perHourPaise);
}
}
class ParkingFeeCalculator {
private FeeStrategy strategy;
ParkingFeeCalculator(FeeStrategy strategy) { this.strategy = strategy; }
void setStrategy(FeeStrategy s) { this.strategy = s; } // swappable at runtime
long charge(long minutes) { return strategy.feePaise(minutes); }
}
class StrategyDemo {
public static void main(String[] args) {
ParkingFeeCalculator calc = new ParkingFeeCalculator(new HourlyFee(4000));
System.out.println(calc.charge(125)); // 3 hours -> 12000
calc.setStrategy(new CappedDailyFee(4000, 20000));
System.out.println(calc.charge(125)); // 3 hours -> 12000 (under the cap)
System.out.println(calc.charge(1800)); // 1 day + 6 hours -> 20000 + 20000 = 40000
}
}
Check the last line: 1800 minutes is 1 full day (1440 minutes) plus 360 minutes = 6 hours. Six hours at ₹40 is ₹240 (24,000 paise), capped at ₹200 (20,000 paise). Total: 20,000 + 20,000 = 40,000 paise.
In Java 8+, a single-method strategy interface is a functional interface, so simple strategies can be lambdas: FeeStrategy free = m -> 0;. Comparator is the JDK's best-known strategy: list.sort(Comparator.comparing(Order::total)).
In Python, functions are first-class, so a strategy is often just a function passed in:
import math
def hourly(rate):
return lambda minutes: max(1, math.ceil(minutes / 60)) * rate
def flat(amount):
return lambda minutes: amount
class FeeCalculator:
def __init__(self, strategy):
self.strategy = strategy
def charge(self, minutes):
return self.strategy(minutes)
print(FeeCalculator(hourly(4000)).charge(125)) # 12000
print(FeeCalculator(flat(5000)).charge(125)) # 5000
When to use, when not to
- Use when there are several variants of one algorithm and the set is expected to grow, or the choice depends on configuration or user input: pricing, discounts, payment methods, spot allocation, split types in Splitwise, rate-limiting algorithms, sort orders.
- Avoid when there are only two fixed variants that will never change — a simple
ifis clearer. Also watch for strategies that need lots of data from the context; that suggests the responsibility is split in the wrong place.
Interview question
"How would you support different pricing for weekdays, weekends and holidays in a movie booking system?" A PricingStrategy interface with one implementation per rule, selected by a small resolver based on the show date (or composed, for example a holiday surcharge wrapping a base price). The booking service depends only on the interface; adding "festival pricing" is a new class plus one line in the resolver.
Observer
Problem
When one object changes, several others need to react, and the changing object should not have to know who they are. Example: when a booking is confirmed, send an SMS, an email, update analytics and award loyalty points. Hard-coding all four calls inside the booking service couples it to every reaction and grows with each new one.
Observer defines a subject that keeps a list of observers (subscribers) and notifies them all when something happens. Observers register and unregister themselves.
Structure
+-------------------------+ +-------------------------+
| BookingEvents (subject) |------->| <<interface>> |
+-------------------------+ 0..* | BookingListener |
| + subscribe(l) | +-------------------------+
| + unsubscribe(l) | | + onConfirmed(booking) |
| + publish(booking) | +-----------^-------------+
+-------------------------+ :
+--------------+-------------+
: : :
+----------+ +-----------+ +-----------+
| SmsAlert | | EmailAlert| | Loyalty |
+----------+ +-----------+ +-----------+
Java code
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
record Booking(String id, String user, long amountPaise) {}
interface BookingListener {
void onConfirmed(Booking b);
}
class BookingEvents {
private final List<BookingListener> listeners = new CopyOnWriteArrayList<>();
void subscribe(BookingListener l) { listeners.add(l); }
void unsubscribe(BookingListener l) { listeners.remove(l); }
void publish(Booking b) {
for (BookingListener l : listeners) {
try {
l.onConfirmed(b);
} catch (RuntimeException e) {
// One failing observer must not stop the others.
System.out.println("listener failed: " + e.getMessage());
}
}
}
}
class SmsAlert implements BookingListener {
public void onConfirmed(Booking b) { System.out.println("SMS to " + b.user() + ": booking " + b.id()); }
}
class LoyaltyPoints implements BookingListener {
public void onConfirmed(Booking b) {
System.out.println(b.user() + " earned " + b.amountPaise() / 10_000 + " points");
}
}
class BookingService {
private final BookingEvents events;
BookingService(BookingEvents events) { this.events = events; }
void confirm(String id, String user, long amountPaise) {
Booking b = new Booking(id, user, amountPaise);
events.publish(b); // the service does not know who is listening
}
}
class ObserverDemo {
public static void main(String[] args) {
BookingEvents events = new BookingEvents();
events.subscribe(new SmsAlert());
events.subscribe(new LoyaltyPoints());
events.subscribe(b -> { throw new RuntimeException("analytics down"); });
new BookingService(events).confirm("BK-1", "asha", 45_000);
}
}
Output: an SMS line, asha earned 4 points (45,000 / 10,000 = 4 with integer division), and listener failed: analytics down.
Design details worth mentioning:
CopyOnWriteArrayListlets observers subscribe or unsubscribe while a notification loop is running, without aConcurrentModificationException.- Isolate failures: one broken observer should not prevent others from being notified.
- Push vs pull: here we push the booking to observers. In the pull style the subject passes only itself, and observers query what they need.
- Synchronous by default: the publisher waits for every observer. A slow email observer slows booking confirmation. For slow reactions, notify asynchronously (an executor) or move to pub-sub.
- Memory leaks: a subject holding observers that are no longer needed keeps them alive (the "lapsed listener" problem). Always provide
unsubscribe.
Observer vs publish-subscribe
These are often confused. The difference is whether a broker sits in the middle.
Observer (in-process, direct) Pub-sub (via a broker)
+---------+ +-----------+ +---------+
| Subject |---> Observer A | Publisher |-->| Topic |
| |---> Observer B +-----------+ | (broker)|
+---------+ +----+----+
subject holds the list |
+----------+------+
v v
Subscriber A Subscriber B
| Observer | Publish-subscribe | |
|---|---|---|
| Coupling | Subject knows observers (through an interface) | Publisher and subscribers know only the topic |
| Middle layer | None | Message broker or event bus |
| Delivery | Usually synchronous, same process | Usually asynchronous, can cross processes and machines |
| Failure handling | In the subject's loop | Broker retries, persistence, dead-letter queues |
| Examples | Swing/JavaFX listeners, an in-memory event list | Kafka, RabbitMQ, Google Pub/Sub, Redis pub/sub |
Pub-sub is Observer scaled out with a broker. In an LLD round, in-process Observer is the right answer; mention that at system scale you would publish to a topic (see message queues).
When to use, when not to
- Use when one change should trigger an open-ended set of reactions: notifications, cache invalidation, UI updates, audit logs, "notify me when available" features.
- Avoid when the order of reactions matters or reactions must succeed together (a transaction) — implicit fan-out makes ordering and failure semantics hard to see. Long chains of observers triggering other observers are notoriously hard to debug.
Interview question
"Implement 'notify me when this product is back in stock'." The product (or an inventory event hub) is the subject; each waiting user's request is an observer registered for that product ID. When stock goes from zero to positive, publish once, notify observers, and unsubscribe them so each user is notified only once.
Command
Problem
You want to treat a request as an object: queue it, log it, schedule it, retry it, or undo it. If the button or menu item calls editor.insert("x") directly, there is nothing to store or reverse.
Command wraps an action and its parameters in an object with an execute() method (and often undo()). An invoker (button, scheduler, queue) runs commands without knowing what they do; a receiver (the editor, the light, the account) does the real work.
Structure
+----------+ holds +------------------------+
| Invoker |--------->| <<interface>> Command |
| (editor | history | + execute() |
| history)| | + undo() |
+----------+ +-----------^------------+
:
+-------------+------------+
: :
+-------------+ +-------------+
| InsertText |----------->| Document |
| DeleteText | receiver | (receiver) |
+-------------+ +-------------+
Java code
A text editor with undo and redo:
import java.util.ArrayDeque;
import java.util.Deque;
class Document {
private final StringBuilder text = new StringBuilder();
void insert(int pos, String s) { text.insert(pos, s); }
String delete(int pos, int len) {
String removed = text.substring(pos, pos + len);
text.delete(pos, pos + len);
return removed;
}
int length() { return text.length(); }
@Override public String toString() { return text.toString(); }
}
interface Command {
void execute();
void undo();
}
class InsertCommand implements Command {
private final Document doc;
private final int pos;
private final String s;
InsertCommand(Document doc, int pos, String s) { this.doc = doc; this.pos = pos; this.s = s; }
public void execute() { doc.insert(pos, s); }
public void undo() { doc.delete(pos, s.length()); }
}
class DeleteCommand implements Command {
private final Document doc;
private final int pos;
private final int len;
private String removed; // remembered so undo can restore it
DeleteCommand(Document doc, int pos, int len) { this.doc = doc; this.pos = pos; this.len = len; }
public void execute() { removed = doc.delete(pos, len); }
public void undo() { doc.insert(pos, removed); }
}
class Editor {
private final Deque<Command> undoStack = new ArrayDeque<>();
private final Deque<Command> redoStack = new ArrayDeque<>();
void run(Command c) {
c.execute();
undoStack.push(c);
redoStack.clear(); // a new action invalidates the redo history
}
void undo() {
if (undoStack.isEmpty()) return;
Command c = undoStack.pop();
c.undo();
redoStack.push(c);
}
void redo() {
if (redoStack.isEmpty()) return;
Command c = redoStack.pop();
c.execute();
undoStack.push(c);
}
}
class CommandDemo {
public static void main(String[] args) {
Document doc = new Document();
Editor ed = new Editor();
ed.run(new InsertCommand(doc, 0, "Hello"));
ed.run(new InsertCommand(doc, 5, " World"));
ed.run(new DeleteCommand(doc, 0, 6));
System.out.println(doc); // World
ed.undo();
System.out.println(doc); // Hello World
ed.undo();
System.out.println(doc); // Hello
ed.redo();
System.out.println(doc); // Hello World
}
}
Two stacks implement undo and redo. Clearing the redo stack on a new action matches what every editor does: once you type something new, the "future" you undid is gone.
Runnable and Callable passed to an ExecutorService are commands without undo: the executor (invoker) queues and runs them without knowing what they do.
When to use, when not to
- Use for undo/redo, job queues and schedulers, macro recording (a list of commands replayed), transactional operations with rollback, and remote-control style "map a button to an action" designs.
- Avoid for simple direct calls with no need to store, queue or reverse them — a command class per method adds ceremony. When a lambda suffices (
Runnable), use it.
Interview question
"Design undo for a drawing app where some operations cannot be reversed easily, such as applying a filter." Use commands with undo() for cheaply reversible operations (move, recolour). For expensive or lossy ones, have the command store a snapshot of the affected state before executing — combining Command with Memento — and restore it on undo. Cap the history length to bound memory.
State
Problem
An object behaves differently depending on its current state, and the code is full of switch (state) blocks repeated in every method. A vending machine's insertCoin, selectProduct and dispense all behave differently in idle, has money and sold out states. Each new state edits every method.
State moves each state's behaviour into its own class. The context holds the current state object and delegates; state objects decide transitions by setting the context's next state.
Structure
+-------------------------+ +-----------------------------+
| VendingMachine (context)|------>| <<interface>> MachineState |
+-------------------------+ state | + insertCoin(m, amount) |
| - stock, balance | | + select(m, item) |
| + setState(s) | +--------------^--------------+
+-------------------------+ :
+--------------------+--------------+
: : :
+-----------+ +---------------+ +-----------+
| IdleState | | HasMoneyState | | SoldOut |
+-----------+ +---------------+ +-----------+
Transitions: Idle --coin--> HasMoney --select(ok)--> Idle
HasMoney --select(stock hits 0)--> SoldOut
Java code
interface MachineState {
void insertCoin(VendingMachine m, int amount);
void select(VendingMachine m, int price);
}
class VendingMachine {
private MachineState state = new IdleState();
int balance;
int stock;
VendingMachine(int stock) {
this.stock = stock;
if (stock == 0) state = new SoldOutState();
}
void setState(MachineState s) { this.state = s; }
String stateName() { return state.getClass().getSimpleName(); }
void insertCoin(int amount) { state.insertCoin(this, amount); }
void select(int price) { state.select(this, price); }
}
class IdleState implements MachineState {
public void insertCoin(VendingMachine m, int amount) {
m.balance += amount;
m.setState(new HasMoneyState());
}
public void select(VendingMachine m, int price) {
System.out.println("insert money first");
}
}
class HasMoneyState implements MachineState {
public void insertCoin(VendingMachine m, int amount) { m.balance += amount; }
public void select(VendingMachine m, int price) {
if (m.balance < price) {
System.out.println("need " + (price - m.balance) + " more");
return;
}
m.stock--;
System.out.println("dispensed, change " + (m.balance - price));
m.balance = 0;
m.setState(m.stock == 0 ? new SoldOutState() : new IdleState());
}
}
class SoldOutState implements MachineState {
public void insertCoin(VendingMachine m, int amount) { System.out.println("sold out, returning " + amount); }
public void select(VendingMachine m, int price) { System.out.println("sold out"); }
}
class StatePatternDemo {
public static void main(String[] args) {
VendingMachine vm = new VendingMachine(1);
vm.select(25); // insert money first
vm.insertCoin(10);
vm.select(25); // need 15 more
vm.insertCoin(20);
vm.select(25); // dispensed, change 5
System.out.println(vm.stateName()); // SoldOutState
vm.insertCoin(10); // sold out, returning 10
}
}
Each state class answers "what does this action mean right now?" Adding a "maintenance" state is a new class plus the transitions into it; the other states' methods are untouched. Since the states here have no fields, you could share one instance of each (for example as constants or enum values) instead of creating new objects on every transition.
A lighter alternative for simple machines is an enum with a transition table, shown in the UML lesson. Use the enum when states only gate which transitions are legal; use State classes when each state has substantial, different behaviour.
When to use, when not to
- Use when behaviour depends heavily on a lifecycle: vending machines, ATMs, elevators, traffic lights, order and booking status, TCP connections, document approval workflows.
- Avoid for two or three states with trivial differences — an enum and a
switchis easier to read. Many tiny state classes can also scatter the transition logic; draw the state diagram first so the transitions stay visible.
Interview question
"What is the difference between State and Strategy? They have the same class diagram." The structure is the same: a context delegates to an interface. In Strategy, the client usually picks the algorithm and it rarely changes; strategies are unaware of each other. In State, the states themselves trigger transitions to other states as events arrive, so the object's behaviour changes over its lifetime without the client choosing.
Template Method
Problem
Several classes follow the same overall algorithm but differ in some steps. Copying the algorithm into each class duplicates the fixed parts, and the copies drift apart. Template Method puts the fixed skeleton in a base-class method (often final) and leaves the varying steps as abstract or overridable hook methods.
Structure
+-----------------------------------+
| <<abstract>> PaymentFlow |
+-----------------------------------+
| + pay(amount) {final} | validate -> authorise
| (the template method) | -> capture -> receipt
| # authorise(amount) {abstract} |
| # capture(amount) {abstract} |
| # receipt(amount) (hook) |
+----------------^------------------+
|
+---------+----------+
| |
+-------------+ +-------------+
| CardFlow | | UpiFlow |
+-------------+ +-------------+
Java code
abstract class PaymentFlow {
// The skeleton is fixed and cannot be overridden.
final String pay(long amountPaise) {
if (amountPaise <= 0) return "invalid amount";
if (!authorise(amountPaise)) return "declined";
String ref = capture(amountPaise);
return receipt(ref, amountPaise);
}
protected abstract boolean authorise(long amountPaise);
protected abstract String capture(long amountPaise);
// Hook with a default; subclasses may override.
protected String receipt(String ref, long amountPaise) {
return "paid " + amountPaise + " ref " + ref;
}
}
class CardFlow extends PaymentFlow {
protected boolean authorise(long a) { return a <= 5_000_000; } // card limit
protected String capture(long a) { return "CARD-001"; }
}
class UpiFlow extends PaymentFlow {
protected boolean authorise(long a) { return a <= 10_000_000; }
protected String capture(long a) { return "UPI-001"; }
@Override protected String receipt(String ref, long a) { return "UPI success " + ref; }
}
class TemplateDemo {
public static void main(String[] args) {
System.out.println(new CardFlow().pay(9_000_000)); // declined
System.out.println(new UpiFlow().pay(9_000_000)); // UPI success UPI-001
}
}
This is the Hollywood principle: "don't call us, we'll call you" — the base class calls the subclass's steps, not the other way round. JDK examples: AbstractList implements most of List on top of your get and size; HttpServlet.service dispatches to your doGet/doPost.
When to use, when not to
- Use when the algorithm's order of steps must be enforced and only some steps vary: data import pipelines (read, validate, transform, save), payment flows, report generation, game turns.
- Avoid when variation is in many steps or combinations — inheritance becomes rigid and you can only have one parent. Strategy (composition) is the flexible alternative: pass the varying steps in as objects. Prefer it when steps vary independently.
Interview question
"Template Method or Strategy for a data-import job with CSV and JSON sources?" If the pipeline order is fixed and only parsing differs, Template Method with an abstract parse step is simple. If parsing, validation and storage vary independently (CSV into MySQL, JSON into S3), inject a strategy for each step instead, avoiding a subclass per combination.
Chain of Responsibility
Problem
A request should be handled by one of several handlers, or pass through several in sequence, and the sender should not know which. Examples: request middleware (authenticate, rate limit, validate), approval levels (team lead up to ₹10,000, manager up to ₹1,00,000, director above), and an ATM dispensing notes from largest to smallest.
Chain of Responsibility links handlers so each either handles the request, passes it to the next, or both.
Structure
Client --> [Handler A] --> [Handler B] --> [Handler C] --> (end)
| | |
handle or handle or handle or
pass on pass on pass on
+-----------------------------+
| <<abstract>> Handler |
+-----------------------------+
| - next: Handler |
| + setNext(h): Handler |
| + handle(request) |
+-----------------------------+
Java code
ATM cash dispensing:
import java.util.LinkedHashMap;
import java.util.Map;
abstract class NoteDispenser {
private NoteDispenser next;
protected final int noteValue;
NoteDispenser(int noteValue) { this.noteValue = noteValue; }
NoteDispenser then(NoteDispenser next) {
this.next = next;
return next; // allows a.then(b).then(c)
}
void dispense(int amount, Map<Integer, Integer> out) {
int count = amount / noteValue;
int remaining = amount % noteValue;
if (count > 0) out.put(noteValue, count);
if (remaining > 0) {
if (next == null) throw new IllegalArgumentException("cannot dispense " + remaining);
next.dispense(remaining, out);
}
}
}
class Notes2000 extends NoteDispenser { Notes2000() { super(2000); } }
class Notes500 extends NoteDispenser { Notes500() { super(500); } }
class Notes100 extends NoteDispenser { Notes100() { super(100); } }
class ChainDemo {
public static void main(String[] args) {
NoteDispenser chain = new Notes2000();
chain.then(new Notes500()).then(new Notes100());
Map<Integer, Integer> out = new LinkedHashMap<>();
chain.dispense(5800, out);
System.out.println(out); // {2000=2, 500=3, 100=3}
try {
chain.dispense(150, new LinkedHashMap<>());
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // cannot dispense 50
}
}
}
Check: 5800 = 2 × 2000 (4000) + 3 × 500 (1500) + 3 × 100 (300) = 5800. For 150, the ₹100 handler gives one note and 50 remains with no next handler, so it fails. A real ATM would validate the amount first (must be a multiple of 100), check note availability in each cassette, and roll back if the full amount cannot be dispensed — good extension points to mention.
JDK and framework examples: servlet Filter chains, Spring Security's filter chain, and logging handler hierarchies.
When to use, when not to
- Use when several handlers might process a request and the set or order should be configurable: middleware pipelines, approval workflows, event bubbling in UI trees, cash dispensing, support ticket escalation.
- Avoid when exactly one fixed handler always processes the request. Also beware of requests silently falling off the end of the chain unhandled; always define what happens at the end (an error or a default handler).
Interview question
"Design an expense approval system: under ₹10,000 the team lead approves, under ₹1,00,000 the manager, otherwise the director." Each approver is a handler with a limit; if the amount is within its limit it approves, otherwise it passes to its successor. The chain is assembled in configuration, so adding a finance-head level is a new handler inserted in the chain with no change to the others.
Iterator
Problem
Clients need to traverse a collection without knowing its internal structure (array, linked list, tree, paged API results). Iterator provides a standard way to step through elements — hasNext() and next() — while the collection hides its representation.
Structure
+-----------------------+ creates +--------------------------+
| <<interface>> |--------->| <<interface>> Iterator<T>|
| Iterable<T> | | + hasNext(): boolean |
| + iterator() | | + next(): T |
+-----------^-----------+ +-------------^------------+
: :
+--------+---------+ +----------+----------+
| Playlist | | PlaylistIterator |
+------------------+ +---------------------+
Java code
Java builds this into the language: anything implementing Iterable works with the for-each loop.
import java.util.Iterator;
import java.util.NoSuchElementException;
class Playlist implements Iterable<String> {
private final String[] songs;
private final boolean shuffleFromMiddle;
Playlist(boolean shuffleFromMiddle, String... songs) {
this.songs = songs;
this.shuffleFromMiddle = shuffleFromMiddle;
}
public Iterator<String> iterator() {
return new Iterator<>() {
private int visited = 0;
private final int start = shuffleFromMiddle ? songs.length / 2 : 0;
public boolean hasNext() { return visited < songs.length; }
public String next() {
if (!hasNext()) throw new NoSuchElementException();
String s = songs[(start + visited) % songs.length];
visited++;
return s;
}
};
}
}
class IteratorDemo {
public static void main(String[] args) {
for (String s : new Playlist(true, "A", "B", "C", "D")) {
System.out.print(s + " "); // C D A B
}
System.out.println();
}
}
The caller writes an ordinary for-each loop and never knows the traversal starts from the middle and wraps around. In Python, the same idea is a generator function using yield, or an object with __iter__ and __next__.
When to use: custom collections, trees (in-order, level-order iterators), lazily fetched pages of API results. When not to: standard collections already provide iterators; streams are often clearer for transformations. Interview question: "What is a fail-fast iterator?" Iterators of ArrayList and HashMap throw ConcurrentModificationException if the collection is structurally modified during iteration other than through the iterator's own remove, detected via an internal modification count. It is a best-effort bug detector, not a thread-safety guarantee.
Mediator
Problem
Many objects talk to each other directly, creating a tangled many-to-many web. Adding one more participant means touching many classes. Mediator introduces a central object through which components communicate, turning many-to-many links into one-to-many.
Structure
Without mediator With mediator
A <---> B A ---+
^ \ / ^ |
| \ / | B ---+--> [ ChatRoom / Tower ]
| X | |
v / \ v C ---+
C <---> D D ---+
Java code
A chat room:
import java.util.ArrayList;
import java.util.List;
class ChatRoom {
private final List<Member> members = new ArrayList<>();
void join(Member m) { members.add(m); }
void broadcast(Member from, String text) {
for (Member m : members) {
if (m != from) m.receive(from.name, text);
}
}
}
class Member {
final String name;
private final ChatRoom room;
Member(String name, ChatRoom room) {
this.name = name;
this.room = room;
room.join(this);
}
void say(String text) { room.broadcast(this, text); } // talks only to the mediator
void receive(String from, String text) { System.out.println(name + " got from " + from + ": " + text); }
}
class MediatorDemo {
public static void main(String[] args) {
ChatRoom room = new ChatRoom();
Member a = new Member("asha", room);
new Member("ravi", room);
new Member("meera", room);
a.say("standup in 5");
}
}
Members never reference each other. The classic real-world analogy is an air-traffic control tower: planes talk to the tower, not to every other plane. In LLD problems, an elevator system's central dispatcher that decides which car serves a floor request is a mediator between floor buttons and elevator cars.
When to use: many interacting peers whose interactions keep changing — chat, UI dialogs where widgets affect each other, dispatchers. When not to: few participants with stable interactions; and watch the mediator itself, which can become a god class. Interview question: "Mediator vs Observer?" Observer is a one-way broadcast from a subject to subscribers who do not talk back. Mediator coordinates two-way interactions among peers and often contains the logic of who should react; a mediator may use observer-style notifications internally.
Memento
Problem
You want to save and restore an object's state — for undo, checkpoints or "cancel changes" — without exposing the object's internals to whoever stores the snapshots. Memento has the object (the originator) produce an opaque snapshot (the memento) that a caretaker stores and later hands back.
Structure
+------------+ save() +------------+ stored by +-----------+
| Originator |---------->| Memento |<----------| Caretaker |
| (Form) |<----------| (opaque, | | (history) |
+------------+ restore() | immutable) | +-----------+
+------------+
Java code
import java.util.ArrayDeque;
import java.util.Deque;
class ProfileForm {
private String name = "";
private String city = "";
void edit(String name, String city) { this.name = name; this.city = city; }
// Memento: immutable, and only ProfileForm knows how to read it.
record Snapshot(String name, String city) {}
Snapshot save() { return new Snapshot(name, city); }
void restore(Snapshot s) { this.name = s.name(); this.city = s.city(); }
@Override public String toString() { return name + " / " + city; }
}
class MementoDemo {
public static void main(String[] args) {
ProfileForm form = new ProfileForm();
Deque<ProfileForm.Snapshot> history = new ArrayDeque<>(); // caretaker
form.edit("Asha", "Pune");
history.push(form.save());
form.edit("Asha R", "Hyderabad");
System.out.println(form); // Asha R / Hyderabad
form.restore(history.pop());
System.out.println(form); // Asha / Pune
}
}
In stricter Java designs, the snapshot type is a private nested class or exposes no getters outside the originator, so the caretaker truly cannot peek. When to use: undo of state that is hard to reverse operation by operation, checkpoints in games or wizards. When not to: large objects snapshotted often — memory grows quickly; prefer Command-based undo storing only deltas. Interview question: "Command-based undo vs Memento-based undo?" Command stores how to reverse each operation (small, but every command needs a correct undo). Memento stores the whole state before a change (simple and always correct, but memory-heavy). Many editors combine them.
Visitor (briefly)
Problem
You have a stable set of element classes (say, Book, Electronics, Grocery in a cart) and keep adding new operations over them (tax calculation, shipping cost, export to JSON). Adding a method to every class for each operation is intrusive. Visitor moves each operation into a visitor class with one visit method per element type; each element has an accept(visitor) method that calls back the right visit. This technique of choosing a method by both the visitor and the element type is called double dispatch.
element.accept(v) ---> v.visit(this) (this has the concrete type)
+---------------------+ +------------------------+
| <<interface>> Item | | <<interface>> Visitor |
| + accept(v) | | + visit(Book) |
+----------^----------+ | + visit(Grocery) |
: +-----------^------------+
+------+------+ :
: : +-------+--------+
[Book] [Grocery] : :
[TaxVisitor] [ShippingVisitor]
import java.util.List;
interface ItemVisitor<R> {
R visitBook(Book b);
R visitGrocery(Grocery g);
}
interface Item {
<R> R accept(ItemVisitor<R> v);
}
record Book(long pricePaise) implements Item {
public <R> R accept(ItemVisitor<R> v) { return v.visitBook(this); }
}
record Grocery(long pricePaise, double kg) implements Item {
public <R> R accept(ItemVisitor<R> v) { return v.visitGrocery(this); }
}
class TaxVisitor implements ItemVisitor<Long> {
public Long visitBook(Book b) { return 0L; } // books exempt in this example
public Long visitGrocery(Grocery g) { return g.pricePaise() * 5 / 100; } // 5%
}
class VisitorDemo {
public static void main(String[] args) {
List<Item> cart = List.of(new Book(50_000), new Grocery(20_000, 2.0));
long tax = 0;
for (Item i : cart) tax += i.accept(new TaxVisitor());
System.out.println(tax); // 1000
}
}
The trade-off is the mirror image of normal polymorphism: new operations are easy (a new visitor), but a new element type forces a change to every visitor. Use Visitor when the element hierarchy is stable and operations multiply — compilers walking syntax trees are the classic case. In modern Java (21+), sealed interfaces with pattern-matching switch often replace Visitor more simply, with the compiler checking that every subtype is handled.
Interview question: "What is double dispatch?" Java picks overridden methods by the runtime type of the receiver only (single dispatch). Visitor gets dispatch on two types by calling element.accept(visitor), which dispatches on the element, and then visitor.visitX(this), which dispatches on the visitor with the element's static type now known exactly.
Pattern → problem → where it shows up
| Pattern | Problem it solves | LLD problems where it shows up |
|---|---|---|
| Strategy | Swap one of several algorithms | Parking fees, spot allocation, movie ticket pricing, Splitwise split types (equal, exact, percentage), rate-limiter algorithms, payment methods |
| Observer | Notify an open set of dependents of a change | Booking confirmations and cancellations, back-in-stock alerts, elevator display updates, stock price watchers |
| Command | Represent actions as objects; undo, queue, log | Text editor undo/redo, elevator requests queue, remote control, job schedulers |
| State | Behaviour depends on lifecycle state | Vending machine, ATM, elevator (moving, idle, doors open), order/booking status, traffic light |
| Template Method | Fixed algorithm skeleton, varying steps | Payment flows, report generation, game turn sequences, data import pipelines |
| Chain of Responsibility | Pass a request along handlers | ATM cash dispenser, expense approvals, request middleware, logging levels, support escalation |
| Iterator | Traverse without exposing structure | Custom collections, playlists, paginated results, tree traversal in file systems |
| Mediator | Centralise many-to-many communication | Elevator dispatcher, chat rooms, air-traffic control, auction house |
| Memento | Snapshot and restore state | Editor undo, game checkpoints, form "discard changes" |
| Visitor | Add operations over a stable type hierarchy | Tax/shipping over cart item types, expression evaluators, file-system reports |
Interview tip
In a case-study round, announce patterns as answers to requirements, not as decoration: "Fees differ by vehicle and may change, so FeeStrategy. The machine's response to a button depends on whether money is inserted, so State. Customers want alerts, so the booking publishes events to observers." One sentence each is enough.
Common mistake
Confusing State with Strategy, or Observer with pub-sub, and being unable to explain the difference when asked. Both pairs share a structure; the difference is who triggers changes (State: the states themselves) and whether a broker decouples sender and receiver (pub-sub: yes).
Interview questions
Q1. When would you use the Strategy pattern? Give an LLD example.
When there are several interchangeable algorithms and the set may grow or the choice is made at runtime. In a parking lot, fee calculation differs by vehicle type and policy, so a FeeStrategy interface with hourly, capped-daily and weekend implementations keeps the calculator closed for modification. New pricing is a new class.
Q2. Strategy vs State?
They share a class diagram. With Strategy, the client chooses an algorithm and the strategies are independent of each other. With State, the current state object handles events and itself decides the next state, so behaviour changes over the object's lifetime as events arrive.
Q3. How does Observer differ from publish-subscribe?
In Observer, the subject holds references to its observers and notifies them directly, usually synchronously in one process. In pub-sub, publishers and subscribers communicate through a broker or topic and do not know each other; delivery is usually asynchronous and can cross machines. Pub-sub adds durability and retries but also latency and operational cost.
Q4. What problems can Observer cause and how do you mitigate them?
A slow or failing observer can block or break the notification loop, so isolate failures and use asynchronous delivery for slow work. Subjects holding forgotten observers cause memory leaks, so provide unsubscribe. Cascades of observers triggering each other are hard to debug, so keep event flows shallow and documented.
Q5. How would you implement undo and redo?
Represent each action as a Command with execute and undo, keep an undo stack and a redo stack, push to undo on execute, move between stacks on undo and redo, and clear redo on a new action. For operations that are hard to reverse, store a Memento snapshot inside the command. Bound the history size.
Q6. Explain the State pattern using a vending machine.
The machine is the context; Idle, HasMoney and SoldOut are state classes implementing the same actions. Inserting a coin in Idle stores it and moves to HasMoney; selecting in Idle asks for money; selecting in HasMoney dispenses and transitions to Idle or SoldOut. Each state handles only its own behaviour, removing switch statements from every method.
Q7. What is the Template Method pattern, and what is its weakness?
A base class defines the algorithm's skeleton in a final method and calls abstract or hook methods that subclasses implement. It enforces step order and removes duplication. Its weakness is inheritance: one parent only, and combinations of variations need many subclasses, so Strategy is preferred when steps vary independently.
Q8. How does Chain of Responsibility apply to an ATM?
Each handler dispenses one denomination: it takes as many notes as possible and passes the remainder to the next handler. ₹5800 becomes two ₹2000, three ₹500 and three ₹100 notes. The chain order and denominations are configurable, and if the last handler has a remainder, the request fails.
Q9. What is a fail-fast iterator?
An iterator that throws ConcurrentModificationException if the underlying collection is structurally modified during iteration by anything other than the iterator itself. ArrayList and HashMap iterators track a modification count to detect this. Concurrent collections like CopyOnWriteArrayList provide snapshot iterators that never throw for this reason.
Q10. When do you use a Mediator, and what is the risk?
When many objects interact in a many-to-many way and the interactions change often, such as chat participants or an elevator dispatcher choosing cars for requests. Components talk only to the mediator, reducing coupling. The risk is that the mediator absorbs all logic and becomes a god class, so keep it focused on coordination.
Q11. Memento vs Command for undo?
Memento saves full state snapshots, which is simple and always correct but uses memory proportional to state size per step. Command stores operations and their inverses, which is compact but needs a correct undo for every operation. Use Memento for small or hard-to-reverse state and Command for large documents with cheap inverse operations.
Q12. What is double dispatch and how does Visitor achieve it?
Double dispatch chooses a method based on the runtime types of two objects. Java natively dispatches only on the receiver, so Visitor calls element.accept(visitor) (dispatching on the element), which calls visitor.visitX(this) (dispatching on the visitor with the element's exact type). Sealed types with pattern-matching switch are a modern alternative.
Q13. Which patterns would you use in an elevator system?
State for each elevator car (idle, moving up, moving down, doors open), Strategy for the scheduling algorithm that picks a car, Command or a request queue for floor and cabin requests, and a Mediator-like dispatcher coordinating cars and floor buttons. Observer can update floor displays when a car moves.
Q14. Is the observer list in your implementation thread-safe?
Using CopyOnWriteArrayList makes subscribe, unsubscribe and iteration safe concurrently, with iteration over a snapshot. It suits listener lists because reads vastly outnumber writes. If writes were frequent, a different structure or explicit locking would be better because each write copies the array.
Key takeaways
- Behavioural patterns decide who does what and who tells whom; they are the backbone of LLD case studies.
- Strategy swaps algorithms (pricing, allocation, split types); State lets lifecycle states drive behaviour and transitions.
- Observer notifies an open set of listeners in-process; pub-sub adds a broker, asynchrony and cross-process delivery.
- Command turns actions into objects for undo, queues and logs; Memento snapshots state for restore.
- Template Method fixes an algorithm's skeleton via inheritance; prefer Strategy when steps vary independently.
- Chain of Responsibility passes requests along handlers — ATM notes, approvals, middleware.
- Iterator hides traversal, Mediator centralises many-to-many talk, Visitor adds operations over a stable hierarchy.
- In interviews, introduce each pattern as the answer to a stated requirement.
Next lesson
Continue with Design a parking lot.

