Behavioral
Patterns
Behavioral patterns deal with algorithms and object communication — how responsibilities are distributed and how objects interact. Click any card to jump to its deep dive.
| PATTERN | TRIGGER / SMELL | KEY MECHANISM | SOLID PRINCIPLE |
|---|---|---|---|
| Strategy | if/else branching on algorithm type | Inject strategy via interface | OCP, DIP |
| Observer | One change → notify many dependents | Register/notify observers | OCP, SRP |
| Chain of Resp. | Multiple potential handlers, unknown upfront | Handlers form a linked chain | SRP, OCP |
| State | switch/if on growing state field | State objects hold behaviour; context delegates | SRP, OCP |
| Command | Need undo/redo, queue, or logging of ops | Command objects with execute() + undo() | SRP, OCP |
| Template Method | Same algorithm, different step implementations | Abstract class; subclasses override steps | OCP, DRY |
| Iterator | Need sequential access to collection internals | Iterator object with hasNext/next | SRP |
| Mediator | N objects all talk directly → N² connections | All communication routes through mediator | SRP, DIP |
| Memento | Need save/restore state without exposing internals | Originator creates opaque Memento; Caretaker stores it | Encapsulation |
| Visitor | Many operations on stable class hierarchy | accept(visitor) → double dispatch | OCP, SRP |
| Null Object | Null checks scattered everywhere | No-op implementation of same interface | SRP, LSP |
| Interpreter | Building expression evaluator or DSL | Grammar as class hierarchy; interpret(context) | OCP |
interface PaymentStrategy { boolean pay(double amount); } class UPIPayment implements PaymentStrategy { private final String upiId; public boolean pay(double amt) { System.out.println("Paid ₹"+amt+" via UPI: "+upiId); return true; } } class WalletPayment implements PaymentStrategy { private double balance; public boolean pay(double amt) { if (balance < amt) { System.out.println("Insufficient"); return false; } balance -= amt; return true; } } // Context — knows nothing about UPI/Wallet internals class ShoppingCart { private PaymentStrategy strategy; public void setStrategy(PaymentStrategy s) { this.strategy = s; } public boolean checkout() { return strategy.pay(getTotal()); // Delegates — no if/else } } // Swap algorithm at runtime: cart.setStrategy(new UPIPayment("ajay@icici")); cart.checkout(); cart.setStrategy(new WalletPayment("PAYTM", 500)); cart.checkout();
interface StockObserver { void update(String symbol, double price, double changePct); } class StockTicker { private final List<StockObserver> observers = new ArrayList<>(); private final Map<String, Double> prices = new HashMap<>(); public void subscribe(StockObserver o) { observers.add(o); } public void unsubscribe(StockObserver o) { observers.remove(o); } public void updatePrice(String sym, double newPrice) { double old = prices.getOrDefault(sym, newPrice); double pct = ((newPrice - old) / old) * 100; prices.put(sym, newPrice); // PUSH model: send data directly to observers observers.forEach(o -> o.update(sym, newPrice, pct)); } } // Concrete observers — totally independent of each other class MobileApp implements StockObserver { public void update(String s, double p, double chg) { System.out.printf("[App] %s: ₹%.2f (%+.2f%%)%n", s, p, chg); } } class AlertService implements StockObserver { public void update(String s, double p, double chg) { if (Math.abs(chg) > 5.0) System.out.println("🚨 BIG MOVE: "+s); } }
Push vs Pull
- Push: subject sends data in update(). Simpler, observer always has data.
- Pull: subject calls update(this). Observer fetches what it needs. More flexible.
Real-world Observers
- → Java EventListener (ActionListener)
- → Spring ApplicationEvent
- → Kafka (producer → consumers)
- → RxJava reactive streams
abstract class CashHandler { protected CashHandler next; // Fluent chaining: h1.setNext(h2).setNext(h3) public CashHandler setNext(CashHandler n) { this.next = n; return n; } public abstract void dispense(int amount); } class TwoThousandHandler extends CashHandler { public void dispense(int amount) { int notes = amount / 2000, rem = amount % 2000; if (notes > 0) System.out.println("Dispensing "+notes+"×₹2000"); if (rem > 0 && next != null) next.dispense(rem); // Pass remainder } } class FiveHundredHandler extends CashHandler { /* similar */ } class HundredHandler extends CashHandler { /* similar */ } // Build the chain CashHandler atm = new TwoThousandHandler(); atm.setNext(new FiveHundredHandler()) .setNext(new HundredHandler()); atm.dispense(3700); // → Dispensing 1×₹2000 | 3×₹500 | 2×₹100
interface VendingMachineState { void insertCoin(VendingMachine m, int amount); void selectProduct(VendingMachine m, String m4-code); void dispense(VendingMachine m); void cancel(VendingMachine m); } class IdleState implements VendingMachineState { public void insertCoin(VendingMachine m, int amt) { m.setAmount(amt); m.setState(new HasMoneyState()); // TRANSITION } public void selectProduct(VendingMachine m, String c) { System.out.println("Insert coin first"); // Invalid in this state } public void dispense(VendingMachine m) { /* invalid */ } public void cancel(VendingMachine m) { /* nothing to cancel */ } } class HasMoneyState implements VendingMachineState { /* ... */ } class DispensingState implements VendingMachineState { /* ... */ } // Context — delegates everything to current state class VendingMachine { private VendingMachineState state = new IdleState(); public void insertCoin(int amt) { state.insertCoin(this, amt); } public void setState(VendingMachineState s) { this.state = s; } }
interface Command { void execute(); void undo(); } class LightOnCommand implements Command { private final Light light; public void execute() { light.turnOn(); } public void undo() { light.turnOff(); } // Inverse operation } class ACTempCommand implements Command { private final AC ac; private final int newTemp; private int prevTemp; // Saved for undo public void execute() { prevTemp = ac.getTemp(); ac.setTemp(newTemp); } public void undo() { ac.setTemp(prevTemp); } } // Invoker — the smart home controller class SmartHomeHub { private final Deque<Command> history = new ArrayDeque<>(); public void execute(Command cmd) { cmd.execute(); history.push(cmd); // Push to undo stack } public void undo() { if (!history.isEmpty()) history.pop().undo(); } } hub.execute(new LightOnCommand(bedroom)); hub.execute(new ACTempCommand(ac, 20)); hub.undo(); // AC reverts to previous temp hub.undo(); // Light turns off
abstract class DataMigrationPipeline { // TEMPLATE METHOD — final: order never changes public final void migrate() { extractData(); // abstract — must override validateData(); // hook — may override transformData(); // abstract — must override loadData(); // abstract — must override notifyDone(); // hook — default OK } protected abstract void extractData(); protected abstract void transformData(); protected abstract void loadData(); protected void validateData() { System.out.println("Schema validation"); } protected void notifyDone() { System.out.println("Migration done"); } } class MySQLToPostgres extends DataMigrationPipeline { protected void extractData() { System.out.println("SELECT * FROM MySQL"); } protected void transformData() { System.out.println("ENUM→text, TINYINT→bool"); } protected void loadData() { System.out.println("COPY INTO Postgres"); } protected void validateData() { System.out.println("Row count + FK check"); } } new MySQLToPostgres().migrate(); // Runs steps in correct order — always
interface Iterator<T> { boolean hasNext(); T next(); } class Playlist { private final List<Song> songs = new ArrayList<>(); public Iterator<Song> createIterator() { return new Iterator<Song>() { int i = 0; public boolean hasNext() { return i < songs.size(); } public Song next() { return songs.get(i++); } }; } } // Client — doesn't know internals are a List Iterator<Song> it = playlist.createIterator(); while (it.hasNext()) play(it.next());
interface ATC { void requestLanding(Aircraft a); void requestTakeoff(Aircraft a); void broadcast(String msg, Aircraft source); } abstract class Aircraft { protected final ATC atc; // Only knows ATC — not other aircraft public void land() { atc.requestLanding(this); } public void takeoff() { atc.requestTakeoff(this); } public abstract void receive(String msg); } // Aircraft talk ONLY to ATC tower — never directly to each other // ATC routes communication, manages runway, notifies all parties
// MEMENTO — opaque snapshot (Caretaker can't read it) class EditorMemento { private final String m4-content; // package-private — only Originator reads private final int cursor; EditorMemento(String c, int pos) { m4-content=c; cursor=pos; } String getContent() { return m4-content; } int getCursor() { return cursor; } } // ORIGINATOR — creates and restores from memento class TextEditor { private StringBuilder m4-content = new StringBuilder(); private int cursor = 0; public EditorMemento save() { return new EditorMemento(m4-content.toString(), cursor); } public void restore(EditorMemento m) { m4-content = new StringBuilder(m.getContent()); cursor=m.getCursor(); } } // CARETAKER — stores mementos, never reads inside them class UndoManager { private final Deque<EditorMemento> stack = new ArrayDeque<>(); public void push(EditorMemento m) { stack.push(m); } public EditorMemento pop() { return stack.pop(); } }
interface TaxVisitor { double visit(Book b); double visit(Electronics e); double visit(Food f); } interface Product { double accept(TaxVisitor v); } // DOUBLE DISPATCH key class Book implements Product { boolean educational; public double accept(TaxVisitor v) { return v.visit(this); } // Passes self } class GSTCalculator implements TaxVisitor { public double visit(Book b) { return b.educational ? 0 : b.price*0.12; } public double visit(Electronics e) { return e.price * 0.18; } public double visit(Food f) { return f.processed ? f.price*0.12 : 0; } } // Add ImportDutyCalculator = new class only — Book/Electronics/Food untouched (OCP)
interface Logger { void log(String msg); void error(String msg); } class ConsoleLogger implements Logger { public void log(String msg) { System.out.println("[LOG] "+msg); } public void error(String msg) { System.err.println("[ERR] "+msg); } } // NULL OBJECT — same interface, does nothing class NullLogger implements Logger { public void log(String msg) { /* no-op */ } public void error(String msg) { /* no-op */ } } class PaymentService { private final Logger log; public PaymentService(Logger log) { this.log = log != null ? log : new NullLogger(); // Never null after this } public void process(double amt) { log.log("Processing ₹"+amt); // Safe — always. No null check. } }
interface Expression { int interpret(Map<String,Integer> ctx); } // TERMINAL — leaves of the AST class NumberExpr implements Expression { private final int n; public int interpret(Map ctx) { return n; } } class VariableExpr implements Expression { private final String name; public int interpret(Map ctx) { return (int) ctx.getOrDefault(name, 0); } } // NON-TERMINAL — composite nodes class AddExpr implements Expression { private final Expression l, r; public int interpret(Map ctx) { return l.interpret(ctx) + r.interpret(ctx); } } // Parse "a + b * 3" → a + (b * 3) Expression expr = new AddExpr( new VariableExpr("a"), new MultiplyExpr(new VariableExpr("b"), new NumberExpr(3))); expr.interpret(Map.of("a", 5, "b", 4)); // → 17
State — Algorithm changes automatically as internal state transitions. States know about each other. Self-transitions.
Template Method — Algorithm skeleton fixed in base class. Only specific steps vary via inheritance. Compile-time decision.
Rule: Who controls the switch? Client→Strategy. Object→State. Compiler→Template.
Mediator — Many-to-many: all peers talk through central hub. Hub coordinates responses. Peers know Mediator, not each other.
Chain of Responsibility — Request travels down a chain. Each handler decides to handle or pass. No hub — linear.
Rule: All notified? Observer. Hub decides routing? Mediator. Linear pass-through? CoR.
Strategy — Encapsulates an ALGORITHM (interchangeable). About HOW the action is performed.
Rule: Need undo/queue/log → Command. Need interchangeable algorithm → Strategy. Both look similar — the intent distinguishes them.
Command undo — Stores inverse operations. Lightweight (stores only what changed). More complex.
When to choose: State changes are small and known → Command undo. State is complex or external → Memento snapshot.
Most real editors use Command undo for efficiency.
The seat lifecycle in BookMyShow is a perfect State pattern example. Each state defines which transitions are valid and which are illegal.
✗ confirmPayment() → error
✗ cancel() → error
✓ timeout() → AVAILABLE
✓ cancel() → AVAILABLE
✗ tryLock() by others → error
✗ tryLock() → error
✗ confirmPayment() → error
✗ all others → error
(terminal state)
class Seat { private volatile SeatState state = SeatState.AVAILABLE; private final ReentrantLock lock = new ReentrantLock(); private String lockedBy; public boolean tryLock(String userId, long timeoutMs) { try { if (lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) { if (state == SeatState.AVAILABLE) { state = SeatState.LOCKED; lockedBy = userId; scheduleLockExpiry(5_000); // Auto-release after 5s return true; } lock.unlock(); // Not available — release lock } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return false; // Seat taken or timeout } }
The capstone project for Module A4. Uses 6 behavioral patterns woven together with real concurrency handling. This is the most complex LLD problem so far.
├── model/ Movie, Show, Screen, Seat, User, Booking, Ticket
├── state/ SeatState enum + state transition logic
├── command/ BookSeatCommand, CancelBookingCommand
├── observer/ BookingObserver, EmailNotifier, SMSNotifier, PushNotifier
├── strategy/ PricingStrategy, WeekendPricing, WeekdayPricing
├── chain/ BookingHandler chain: Availability → Payment → Confirm → Notify
├── service/ BookingService, PaymentService, NotificationService
├── facade/ BookingFacade — single public entry point
└── BookMyShowDemo.java 5 concurrent users, 1 show, race condition demo
Identify the correct Behavioral pattern. 2-sentence justification each.
1. Text editor needs Ctrl+Z undo for bold, italic, insert, delete. 2. Social media notifies followers when user posts. Follower list changes dynamically. 3. Loan application: Credit Check → Income Verify → Background → Approval. 4. Traffic light cycles RED → GREEN → YELLOW. Valid actions differ per phase. 5. Zip utility supports DEFLATE/BZIP2/LZMA, switchable per file type at runtime. 6. Shopping cart: calculate total price, weight, customs duty as separate passes without adding methods to Product classes.
Build a generic publish/subscribe EventBus with thread safety.
API: subscribe(Class<T> eventType, Consumer<T> handler) unsubscribe(Class<T> eventType, Consumer<T> handler) publish(T event) Events: OrderPlaced, PaymentFailed, ItemShipped Requirements: - Multiple handlers per event type - Thread-safe: concurrent publish() + subscribe() calls - Handlers run asynchronously (use ExecutorService) - Failed handler must not block other handlers - Unsubscribe mid-flight must not cause ConcurrentModificationException
Combine Template Method for pipeline structure with Strategy for delivery.
Template Method skeleton (in abstract base): gatherData() → processData() → formatOutput() → deliver() Subclasses override formatOutput(): HTMLReportGenerator → formatOutput() returns HTML string PDFReportGenerator → formatOutput() returns byte[] Strategy for deliver() (injected, runtime-swappable): EmailDelivery — sends via SMTP SlackDelivery — sends via Slack webhook Show all 4 combinations work: new HTMLReportGenerator(new EmailDelivery()).generate() new HTMLReportGenerator(new SlackDelivery()).generate() new PDFReportGenerator(new EmailDelivery()).generate() new PDFReportGenerator(new SlackDelivery()).generate()
Complete LLD implementation. The concurrency handling is the critical differentiator.
Implement all 6 pattern usages: State: Seat state machine (AVAILABLE→LOCKED→BOOKED→CANCELLED) Observer: Booking confirmation/cancellation notifications Command: BookSeatCommand + CancelBookingCommand with undo() Strategy: Pricing (Weekend 1.5x, Holiday 2x, Weekday 1.0x) CoR: Availability→Payment→Confirm→Notify handler chain Facade: BookingFacade.bookSeats(userId, showId, seatIds) Demo: 5 threads simultaneously try to book the last 2 seats → Only 2 succeed, 3 get "seat unavailable" → No double booking under any timing Deliverable: 1. Full Java implementation (all classes) 2. Concurrency test showing thread-safe behaviour 3. UML class diagram with all 6 patterns annotated