Beyond the classic Gang of Four patterns, several design patterns appear frequently in object-oriented design interviews and production code. This guide covers the patterns most commonly tested at senior engineering interviews: Iterator, Composite, Command, State, Template Method, and Dependency Injection — with practical examples and when to apply each.
Iterator Pattern
The Iterator pattern provides a standard way to traverse a collection without exposing its internal structure. The collection implements an Iterable interface with a method that returns an Iterator. The Iterator has hasNext() and next() methods. Why it matters: the same traversal code works for arrays, linked lists, trees, hash maps, and any custom collection. The client code does not know (or care) about the underlying data structure. Java: every Collection implements Iterable. The enhanced for-loop (for item : collection) uses iterators internally. Python: the __iter__ and __next__ dunder methods implement the iterator protocol. The for loop calls these automatically. Interview application: “Design an iterator that flattens a nested list” (LeetCode 341). The NestedIterator holds a stack of iterators. hasNext() checks if any iterator has elements; next() returns the next element, diving into nested lists as needed. “Design a peeking iterator” wraps an existing iterator and caches the next element for peek() without consuming it. These problems test your ability to implement the Iterator interface with custom traversal logic.
Composite Pattern
The Composite pattern lets you treat individual objects and compositions of objects uniformly. A Component interface defines operations. Leaf implements the operations directly. Composite holds a collection of Components and delegates operations to its children. Example: a file system. FileSystemComponent has getSize(). File (leaf) returns its size. Directory (composite) returns the sum of its children sizes. Client code calls getSize() on any component without knowing if it is a file or directory. Interview application: “Design a file system” — directories contain files and other directories. Operations (getSize, search, list) work recursively through the tree. “Design an expression evaluator” — Number is a leaf, Add/Multiply are composites containing two sub-expressions. evaluate() on a composite recursively evaluates its children and combines the results. The Composite pattern is the natural choice whenever the problem involves a tree structure where operations apply to both leaves and branches.
Command Pattern
The Command pattern encapsulates a request as an object, allowing you to parameterize, queue, log, and undo operations. A Command interface has execute() and optionally undo(). Concrete commands implement these by calling methods on a receiver object. An Invoker holds and triggers commands without knowing what they do. Use cases: (1) Undo/redo — each user action creates a Command object pushed onto a history stack. Undo pops the stack and calls undo(). Redo pushes it back and calls execute(). Text editors, drawing apps, and IDEs use this pattern. (2) Macro recording — record a sequence of commands, replay them as a single macro. (3) Task queuing — commands are serialized and placed in a queue for later execution (distributed task systems). Interview application: “Design an undo/redo system for a text editor.” Operations: InsertCommand(position, text), DeleteCommand(position, text). Each stores enough state to reverse itself. The editor maintains undo and redo stacks. On undo: pop from undo stack, call undo(), push to redo stack. On redo: pop from redo stack, call execute(), push to undo stack.
State Pattern
The State pattern allows an object to change its behavior when its internal state changes. Instead of large if/else or switch statements checking the current state, each state is a separate class implementing a common interface. The context object delegates behavior to the current state object. Example: a vending machine with states: Idle, HasMoney, Dispensing, OutOfStock. Each state handles insertCoin(), selectProduct(), and dispense() differently. In Idle state, insertCoin() transitions to HasMoney. In HasMoney, selectProduct() transitions to Dispensing. Adding a new state (e.g., Maintenance) requires creating a new class without modifying existing states — Open/Closed Principle. Interview application: “Design a vending machine” or “Design an elevator system.” Both have complex state transitions. Model each state as a class. Define transitions between states. The State pattern makes the transitions explicit and each state self-contained. State machines are also used in: network protocol implementations (TCP state machine), game character behavior (idle, walking, attacking, dying), and workflow engines (order processing: pending -> paid -> shipped -> delivered).
Template Method and Dependency Injection
Template Method: defines the skeleton of an algorithm in a base class, deferring specific steps to subclasses. The base class method calls abstract methods that subclasses implement. Example: a DataProcessor base class with process() that calls readData(), transformData(), writeData(). CSVProcessor and JSONProcessor override these abstract methods with format-specific logic. The overall flow (read -> transform -> write) is defined once. Interview application: “Design a report generator” that supports multiple output formats. The template method defines: fetchData() -> formatData() -> deliverReport(). Subclasses implement format-specific logic. Dependency Injection (DI): instead of a class creating its own dependencies, they are provided (injected) from outside — typically through the constructor. Benefits: (1) Testability — inject mock dependencies for unit testing. (2) Flexibility — swap implementations without changing the class. (3) Decoupling — the class depends on an interface, not a concrete implementation. DI containers (Spring, Guice) manage object creation and injection automatically. In interviews: when asked about making code testable, mention DI as the primary technique. “Instead of calling new DatabaseConnection() inside the class, inject it via the constructor so tests can pass a mock.”