Design Patterns Explained: The Gang-of-Four Patterns That Show Up in LLD Rounds
Design patterns explained for interviews and low-level design: the Gang of Four patterns — factory, builder, singleton, adapter, decorator, strategy, observer, state, template method, command.
A carpenter framing a staircase doesn’t rediscover trigonometry on the job. There’s a standard way to lay out the stringers, cut the treads, and get the rise-over-run right — a plan every tradesperson already knows by name. Say "cut-string stair" on a site and nobody needs the geometry explained; the words carry the whole method. That shared shorthand is the only reason a crew can build fast without re-arguing first principles on every house.
Design patterns are exactly that shelf of plans, but for code. Each one is a named, battle-tested arrangement of classes that solves a recurring design problem — and the name is half the value, because in an interview "I’d make pricing a Strategy" says in three words what would otherwise take three minutes of hand-waving. By the end of this article you and I will have walked the ten patterns that actually turn up in low-level-design rounds, grouped into their three families, each with the problem it kills, a tiny tested example, and — just as important — when not to reach for it.
Let’s start nowhere near a computer
Picture a tradesperson’s shelf of blueprints. There’s one for framing a staircase, one for hanging a door so it swings true, one for a roof truss. None of them is your house — they’re templates. When a new job lands, you don’t invent a way to hang a door; you pull the door-hanging plan and adapt its measurements to this doorway.
Two things about that shelf matter, and they’re both true of design patterns:
- A pattern is a plan, not a part. You can’t buy "a staircase blueprint" off a shelf and bolt it into a wall — it’s a way of arranging real parts. Likewise a pattern isn’t a class you import; it’s a shape you give your own classes.
- The name is the point. The blueprint’s real power on a busy site is that everyone recognises it instantly. Patterns give designers a shared vocabulary — the fastest way to describe a structure to another engineer is to name its pattern.
Here’s the trap the shelf sets, and it fails more interviews than forgetting a pattern ever will: a shelf full of plans tempts you to use one on every job. Patterns are a vocabulary to reach for when a problem demands it — not a checklist to cram into every design. A staircase blueprint on a bungalow with no upstairs is just clutter. We’ll come back to this "gratuitous pattern" anti-pattern at the end, because naming it is a senior move.
Where you already meet them
You don’t have to go looking for patterns — the standard library is built out of them, and you’ve used every one below without naming it:
- Strategy —
Collections.sort(list, comparator). You handsortaComparator; the algorithm for how to order is a swappable object. - Builder —
StringBuilder, and theHttpRequest.newBuilder()...build()chains. A long, optional-heavy construction done one call at a time. - Decorator —
new BufferedReader(new InputStreamReader(in)). Each wrapper adds behaviour (buffering, decoding) around the sameReaderinterface. - Factory Method —
Calendar.getInstance(),LocalDate.of(...). You ask for an instance; which concrete class you get is hidden. - Observer — every UI listener (
button.addActionListener(...)) and the whole publish-subscribe world. - Singleton —
Runtime.getRuntime(): one instance, handed to everyone.
The lesson isn’t "patterns are academic." It’s the opposite — they’re so useful that the platform you build on is made of them. Learning the names just lets you see the machine you’re already driving.
The three families
The Gang of Four sorted their twenty-three patterns into three buckets by the kind of problem each solves. You only need to keep the buckets straight to know where to look:
- Creational — patterns about how objects get made, when plain
newis too rigid: Factory Method, Builder, Singleton (and Abstract Factory). - Structural — patterns about how objects compose into bigger structures: Adapter, Decorator.
- Behavioural — patterns about how objects talk and divide responsibility at runtime: Strategy, Observer, State, Template Method, Command.
We’ll take them family by family. For each, the same three beats: the problem it kills, a snippet you could run, and the "don’t" that keeps you from over-using it. To keep the examples talking to each other, they all live in one coffee shop.
Creational — making objects without hard-coding new
Factory Method — hide which class you build
The problem it kills. Sprinkling new Latte(), new Espresso() across your code welds every caller to the concrete classes. Add a Mocha and you hunt down every construction site. A factory gives callers one door to knock on: they name what they want, and the choosing lives in one place.
interface Beverage {
String name();
int paise();
}
final class Espresso implements Beverage {
public String name() { return "Espresso"; }
public int paise() { return 12000; }
}
final class Latte implements Beverage {
public String name() { return "Latte"; }
public int paise() { return 18000; }
}
final class BeverageFactory {
static Beverage create(String kind) {
return switch (kind) {
case "espresso" -> new Espresso();
case "latte" -> new Latte();
default -> throw new IllegalArgumentException("unknown drink: " + kind);
};
}
}
// BeverageFactory.create("latte").name() → "Latte"The rule: callers depend on the Beverage interface and a string, never on the concrete class. New drinks change exactly one file — the factory.
Abstract Factory in one line: when you need a whole matching family of products at once — a WinterMenu that makes a hot cup, a hot lid, and a seasonal drink that all go together — you promote the factory itself to an interface (MenuFactory) with SummerMenu / WinterMenu implementations. It’s a factory of factories.
When not to reach for it. If there’s exactly one concrete class and no prospect of a second, new Latte() is honest and clearer. A factory that only ever returns one type is ceremony.
Builder — assemble an object one readable step at a time
The problem it kills. A constructor with eight parameters — half of them optional, three of them boolean — is a riddle at the call site: new Coffee("L", 2, true, false, true, ...). Which true was decaf? Builder turns that into named, optional steps and hands back an immutable object at the end.
public final class Coffee {
private final String size;
private final int shots;
private final boolean oatMilk;
private final boolean decaf;
private Coffee(Builder b) {
this.size = b.size;
this.shots = b.shots;
this.oatMilk = b.oatMilk;
this.decaf = b.decaf;
}
public static Builder ofSize(String size) { return new Builder(size); }
@Override public String toString() {
return size + (decaf ? " decaf" : "") + ", " + shots + " shot(s)"
+ (oatMilk ? ", oat milk" : "");
}
public static final class Builder {
private final String size; // required → constructor arg
private int shots = 1; // optional → sensible default
private boolean oatMilk = false;
private boolean decaf = false;
private Builder(String size) { this.size = size; }
public Builder shots(int n) { this.shots = n; return this; }
public Builder oatMilk() { this.oatMilk = true; return this; }
public Builder decaf() { this.decaf = true; return this; }
public Coffee build() { return new Coffee(this); }
}
}
// Coffee.ofSize("Large").shots(2).oatMilk().build().toString()
// → "Large, 2 shot(s), oat milk"The rule: required fields go through the builder’s constructor; optional fields are chainable methods with defaults. The call site reads like a sentence, and the finished Coffee is immutable.
When not to reach for it. Two or three fields, all required? A plain constructor (or a record) is shorter and just as clear. Builders earn their keep with many fields, especially optional ones.
Singleton — one instance, safely, for everyone
The problem it kills. Some things should exist exactly once — a menu loaded from disk, a connection pool. Singleton guarantees a single shared instance behind a global access point. The whole difficulty is making that thread-safe without paying a lock on every read.
The cleanest thread-safe version is the initialization-on-demand holder idiom. It leans on a guarantee of the class loader: a nested class isn’t loaded until it’s first referenced, and class loading is itself thread-safe.
public final class Menu {
private Menu() { /* load prices once, here */ }
private static final class Holder {
private static final Menu INSTANCE = new Menu();
}
public static Menu get() {
return Holder.INSTANCE; // Holder loads on first call — lazy AND safe
}
}No synchronized, no volatile, lazy and correct — the JVM does the hard part. The older alternative is double-checked locking, worth recognising because interviewers ask for it by name:
private static volatile Menu instance; // volatile is NOT optional
public static Menu get() {
if (instance == null) { // 1st check — no lock, fast path
synchronized (Menu.class) {
if (instance == null) { // 2nd check — inside the lock
instance = new Menu();
}
}
}
return instance;
}The rule: the volatile is load-bearing. Without it, another thread can see a half-constructed object through the reordered write — the classic broken singleton. The holder idiom sidesteps the whole hazard, which is why it’s the one to reach for.
Singleton is the most over-used pattern there is, and interviewers know it. A singleton is a global variable in a nicer coat: it hides dependencies (callers don’t declare they use it), and it wrecks testability (you can’t swap it for a fake). Prefer passing one shared instance in through the constructor — dependency injection — and let a framework own its lifetime. Reach for a hand- rolled singleton only for genuinely process-wide, stateless things.
When not to reach for it. Almost always, if a framework (Spring) can hold a single bean for you instead. Never for something with mutable state you’ll want to isolate in tests.
Structural — composing objects into bigger shapes
Adapter — make a foreign interface fit your socket
The problem it kills. You depend on a PaymentProcessor interface, but the payment SDK you’re handed speaks a different shape — charge(double, String) instead of pay(int) — and you can’t edit its code. Adapter is the plug that makes their prongs fit your socket, without either side changing.
// what your app already speaks:
interface PaymentProcessor {
boolean pay(int paise);
}
// the third-party SDK — a different shape you can't change:
final class AcmeGateway {
boolean charge(double rupees, String currency) {
return rupees > 0; // pretend it hits the network
}
}
// the adapter translates one shape into the other:
final class AcmeAdapter implements PaymentProcessor {
private final AcmeGateway gateway = new AcmeGateway();
public boolean pay(int paise) {
return gateway.charge(paise / 100.0, "INR"); // translate units + args
}
}
// new AcmeAdapter().pay(18000) → trueThe rule: the adapter implements your interface and delegates to their object, translating between the two. Your code keeps talking to PaymentProcessor; swapping Acme for a different gateway is a new adapter, nothing else.
When not to reach for it. If you own both sides, don’t adapt — just fix the interface. Adapter is for the boundary with code you can’t change.
Decorator — stack behaviour in wrapping layers
The problem it kills. Milk, sugar, an extra shot — every combination is a variant. Making a subclass per combination (LatteWithMilkAndSugar) explodes into dozens of classes. Decorator adds each responsibility as a wrapper that implements the same interface as the thing it wraps, so you compose behaviour at runtime instead of at compile time.
interface Beverage {
String description();
int paise();
}
final class Espresso implements Beverage {
public String description() { return "Espresso"; }
public int paise() { return 12000; }
}
// a decorator holds a Beverage and IS a Beverage
abstract class Extra implements Beverage {
protected final Beverage base;
protected Extra(Beverage base) { this.base = base; }
}
final class Milk extends Extra {
Milk(Beverage base) { super(base); }
public String description() { return base.description() + " + milk"; }
public int paise() { return base.paise() + 2000; }
}
final class Sugar extends Extra {
Sugar(Beverage base) { super(base); }
public String description() { return base.description() + " + sugar"; }
public int paise() { return base.paise() + 500; }
}
// new Sugar(new Milk(new Espresso())).description() → "Espresso + milk + sugar"
// new Sugar(new Milk(new Espresso())).paise() → 14500The rule: each wrapper is-a Beverage and has-a Beverage. It calls the thing it wraps, then adds its own bit — so wrappers nest to any depth, in any order, and the core is never edited. This is why decoration beats subclassing for "optional add-ons that combine."
When not to reach for it. A fixed, small set of variations that never combine is fine as plain fields or subclasses. Decorator earns its place when the combinations are open-ended.
Behavioural — how objects share the work at runtime
Strategy — swap the algorithm behind one interface
The problem it kills. A pricing method riddled with if (happyHour) ... else if (memberDiscount) ... grows a new branch for every rule and can’t be tested in isolation. Strategy lifts "the algorithm" into its own interface with interchangeable implementations, and lets the caller hold whichever one it needs.
interface DiscountStrategy {
int apply(int paise);
}
final class NoDiscount implements DiscountStrategy {
public int apply(int paise) { return paise; }
}
final class PercentOff implements DiscountStrategy {
private final int percent;
PercentOff(int percent) { this.percent = percent; }
public int apply(int paise) { return paise - paise * percent / 100; }
}
final class HappyHour implements DiscountStrategy {
public int apply(int paise) { return paise / 2; }
}
final class Till {
private DiscountStrategy discount = new NoDiscount();
void use(DiscountStrategy d) { this.discount = d; } // swap at runtime
int total(int paise) { return discount.apply(paise); }
}
// new PercentOff(10).apply(20000) → 18000
// new HappyHour().apply(20000) → 10000The rule: the Till depends on the DiscountStrategy interface, not on any rule. Add a "buy-one-get-one" rule and you write one new class — the Till never changes. That’s the open-closed principle from SOLID made concrete.
When not to reach for it. One algorithm that will never vary doesn’t need an interface. And if the "strategy" is a single method, a lambda or method reference is often the whole pattern — no class hierarchy required.
Observer — one subject, many subscribers
The problem it kills. When an order is ready, the customer’s pager, the kitchen screen, and an SMS all need to know — and you don’t want the order hard-wired to each of them. Observer lets a subject keep a list of listeners and broadcast to them, so you add or remove subscribers without touching the subject.
interface OrderListener {
void onReady(int orderId);
}
final class OrderBoard {
private final List<OrderListener> listeners = new ArrayList<>();
void subscribe(OrderListener l) { listeners.add(l); }
void markReady(int orderId) {
for (OrderListener l : listeners) { // fan the event out to everyone
l.onReady(orderId);
}
}
}
// board.subscribe(id -> System.out.println("Pager buzzes for #" + id));
// board.subscribe(id -> System.out.println("Kitchen clears #" + id));
// board.markReady(42);
// Pager buzzes for #42
// Kitchen clears #42The rule: the subject knows the OrderListener interface and a list — nothing about who’s on it. That decoupling is the whole point: publisher and subscribers evolve independently.
When not to reach for it. For a single, fixed listener, a plain method call is simpler and easier to follow. And beware the debugging cost: a broadcast to many listeners hides who reacts and in what order, which can turn a simple bug into a scavenger hunt.
State — let the object’s state carry its own behaviour
The problem it kills. An order behaves differently when it’s PLACED, PREPARING, READY, or COLLECTED, and encoding that as a thicket of if (status == ...) in every method rots fast. The State pattern gives each state its own object that knows which transition it allows — so changing state literally swaps behaviour.
interface OrderState {
OrderState next(); // the transition this state permits
String label();
}
enum Flow implements OrderState {
PLACED { public OrderState next() { return PREPARING; } },
PREPARING { public OrderState next() { return READY; } },
READY { public OrderState next() { return COLLECTED; } },
COLLECTED { public OrderState next() { return this; } }; // terminal
public String label() { return name(); }
}
final class Order {
private OrderState state = Flow.PLACED;
void advance() { state = state.next(); }
String status() { return state.label(); }
}
// Order o = new Order();
// o.status(); → "PLACED"
// o.advance(); o.status(); → "PREPARING"
// o.advance(); o.advance();
// o.status(); → "COLLECTED"The rule: each state owns its transitions; the context just delegates. Illegal jumps become impossible because a state simply doesn’t offer a transition it shouldn’t — no scattered guards. This is the same discipline that keeps a game object out of nonsense states.
When not to reach for it. Two states and one flag? A boolean is fine. State earns its weight when transitions and per-state behaviour genuinely multiply.
Template Method — fix the skeleton, vary one step
The problem it kills. Brewing tea and brewing coffee share a skeleton — boil water, do the drink-specific bit, pour — and only the middle step differs. Template Method puts the fixed sequence in a final method on a base class and leaves the varying step as an abstract hook, so subclasses can’t reorder the recipe.
abstract class Brew {
// the skeleton is final — subclasses can't reorder it
public final String make() {
return boilWater() + " → " + brew() + " → " + pour();
}
private String boilWater() { return "boil"; }
private String pour() { return "pour"; }
protected abstract String brew(); // the one step that varies
}
final class Tea extends Brew {
protected String brew() { return "steep tea"; }
}
final class Coffee extends Brew {
protected String brew() { return "drip coffee"; }
}
// new Tea().make() → "boil → steep tea → pour"The rule: the base class owns the order; subclasses own the steps. It’s Strategy’s inheritance-based cousin — Strategy composes a swappable object, Template Method overrides a swappable method.
When not to reach for it. If the steps vary independently or you want to swap them at runtime, prefer Strategy (composition) over the rigid inheritance Template Method locks you into.
Command — turn a request into an object
The problem it kills. You want to queue actions, log them, or undo them — but a bare method call is a one-shot event you can’t store or reverse. Command wraps "do this" as an object with execute() (and often undo()), so requests become first-class things you can stash in a list, replay, or roll back.
interface Command {
void execute();
void undo();
}
final class Cart { int items = 0; }
final class AddItem implements Command {
private final Cart cart;
AddItem(Cart cart) { this.cart = cart; }
public void execute() { cart.items++; }
public void undo() { cart.items--; }
}
final class Barista {
private final Deque<Command> history = new ArrayDeque<>();
void run(Command c) { c.execute(); history.push(c); }
void undoLast() { if (!history.isEmpty()) history.pop().undo(); }
}
// Cart cart = new Cart();
// Barista b = new Barista();
// b.run(new AddItem(cart)); b.run(new AddItem(cart)); cart.items → 2
// b.undoLast(); cart.items → 1The rule: a request becomes an object with execute/undo, and a history stack gives you undo for free. This is the backbone of editors, task queues, and transactional workflows.
When not to reach for it. If you never need to queue, log, or undo — just call the method. Command is overhead you buy only for those capabilities.
A pattern per problem, at a glance
Ten patterns is a lot to hold. The way to remember them isn’t as a list — it’s as a set of answers to "what varies?" Each pattern isolates one axis of change so the rest of your code stays still:
| Pattern | Family | The problem it kills | Skip it when… |
|---|---|---|---|
| Factory Method | Creational | callers welded to concrete classes | there’s only ever one concrete type |
| Builder | Creational | telescoping, unreadable constructors | two or three required fields — use a record |
| Singleton | Creational | need exactly one shared instance | a framework (DI) can own the single instance |
| Adapter | Structural | a foreign interface won’t fit your socket | you own both sides — just fix the interface |
| Decorator | Structural | combinatorial subclass explosion | a small, fixed set of variants that don’t combine |
| Strategy | Behavioural | a switch of interchangeable algorithms | the algorithm never varies |
| Observer | Behavioural | subject hard-wired to every consumer | a single, fixed listener |
| State | Behavioural | if (status == …) scattered everywhere | two states and one boolean flag |
| Template Method | Behavioural | duplicated skeletons, one step differs | steps vary at runtime — use Strategy |
| Command | Behavioural | need to queue, log, or undo requests | a plain one-shot method call is enough |
The pattern is a way to make one thing swappable. If nothing’s swapping, you don’t need the pattern — and that judgement, not the memorised list, is what the round is testing.
The complete implementation
Here’s a slice assembled and runnable — Factory hides construction, Decorator stacks the extras, Strategy prices the result — the three most common patterns cooperating on one order:
package dev.fiveyear.patterns;
public final class CoffeeShop {
private CoffeeShop() {}
// ---- product hierarchy the Decorator wraps ----
public interface Beverage {
String description();
int paise();
}
public static final class Espresso implements Beverage {
public String description() { return "Espresso"; }
public int paise() { return 12000; }
}
public static final class Latte implements Beverage {
public String description() { return "Latte"; }
public int paise() { return 18000; }
}
abstract static class Extra implements Beverage {
protected final Beverage base;
Extra(Beverage base) { this.base = base; }
}
public static final class Milk extends Extra {
public Milk(Beverage base) { super(base); }
public String description() { return base.description() + " + milk"; }
public int paise() { return base.paise() + 2000; }
}
public static final class Sugar extends Extra {
public Sugar(Beverage base) { super(base); }
public String description() { return base.description() + " + sugar"; }
public int paise() { return base.paise() + 500; }
}
// ---- creation hidden behind a door (Factory Method) ----
public static Beverage create(String kind) {
return switch (kind) {
case "espresso" -> new Espresso();
case "latte" -> new Latte();
default -> throw new IllegalArgumentException("unknown drink: " + kind);
};
}
// ---- the pricing algorithm, swappable (Strategy) ----
public interface Discount {
int apply(int paise);
}
public static final Discount NONE = paise -> paise;
public static Discount percentOff(int percent) {
return paise -> paise - paise * percent / 100;
}
public static final Discount HAPPY_HOUR = paise -> paise / 2;
}And a run of the shop, with the exact console output in the comments:
import dev.fiveyear.patterns.CoffeeShop;
import dev.fiveyear.patterns.CoffeeShop.Beverage;
// Factory builds it; Decorators wrap it; Strategy prices it.
Beverage drink = new CoffeeShop.Sugar(
new CoffeeShop.Milk(
CoffeeShop.create("latte")));
int full = drink.paise();
int happy = CoffeeShop.HAPPY_HOUR.apply(full);
System.out.println(drink.description()); // Latte + milk + sugar
System.out.println(full); // 20500
System.out.println(CoffeeShop.percentOff(10).apply(full)); // 18450
System.out.println(happy); // 10250Three patterns, one flow, and not one if deciding a type or a price. Adding a Mocha, a Cream extra, or a MemberDiscount each touches exactly one place — which is the entire promise the patterns were bought for.
The interview corner
Design-patterns questions rarely ask you to recite the catalogue. They probe whether you know when each applies — and, more tellingly, when it doesn’t.
Ask these before you name a pattern:
- "What’s actually going to change here?" A pattern isolates one axis of variation. If you can’t name the axis, you don’t need the pattern yet — say so.
- "Is this a pattern the language already gives me for free?" A single-method Strategy is a lambda; a Singleton is a Spring bean. Reaching for the textbook version when the platform hands it to you reads as junior.
- "Are we optimising for readability now, or extensibility later?" Patterns trade a little indirection today for cheap change tomorrow. If the change never comes, that indirection is pure cost.
The follow-up ladder — where a strong candidate keeps climbing:
- "Is a pattern the same as a principle?" No — a principle (like SOLID’s open-closed) is the goal; a pattern is one reusable means to it. Strategy is how you make pricing open-closed. You can honour the principles with no named pattern at all.
- "Strategy and State look identical — same diagram, an interface with implementations. What’s the difference?" Intent. Strategy’s implementations are interchangeable and chosen from outside (the client picks the algorithm). State’s implementations are not interchangeable — each knows the next state, so the objects drive their own transitions from inside. Same shape, opposite control of who swaps.
- "Factory vs Builder — both make objects. When each?" Factory decides which class to instantiate (you want a
Beverage, don’t care which). Builder decides how to configure one class with many optional fields (you know it’s aCoffee, you’re assembling its parts). Different question: what type vs what configuration. - "You used three patterns on a tic-tac-toe board. Defend it." Usually you can’t — and admitting it is the point. A warm-up game with two human players and fixed rules has nothing that varies, so plain code is the senior answer. Naming a seam you would add ("a
MoveStrategyif a computer player is requested") beats building it now. - "How do patterns relate to the four pillars of OOP?" They’re polymorphism and composition, applied with intent. Nearly every behavioural pattern is "program to an interface, hold the varying part as a field" — the pillars are the alphabet, patterns are the words.
Three mistakes that fail the round:
- Pattern-stuffing. Forcing a Factory, a Singleton, and an Observer into a problem that has one class and no events. It signals you reach for patterns as decoration, not as answers to variation. Fewer, justified patterns always beat more.
- A Singleton for mutable shared state. It’s a global variable that hides its dependency and blocks testing. Interviewers treat an un-asked-for singleton as a red flag; prefer injecting a shared instance.
- Confusing Strategy and State (or Factory and Builder). Drawing the right boxes but naming the wrong pattern shows you memorised diagrams, not intent. The distinction is the knowledge being tested.
Where to go from here
You now own the spine: a pattern names one axis of change and isolates it behind an interface — reach for one only when that axis is really moving. Three natural next stops:
- The principles beneath the patterns. Every pattern here is SOLID made concrete — open-closed via Strategy, dependency-inversion via Factory. Learn the principles and you’ll derive patterns instead of memorising them.
- The pillars they’re built from. Patterns are disciplined object-oriented programming — polymorphism and composition with a purpose. Solid on the pillars, the patterns stop feeling like magic.
- See them earn their keep in a real design. Work a machine-coding problem like snake and ladder or tic-tac-toe and notice which patterns genuinely apply — and how often the honest answer is "none yet."
Next time an interviewer asks how you’d structure the pricing, or the notifications, or the game states, you won’t reach for a class diagram from scratch. You’ll pull a blueprint off the shelf, name it, and adapt its measurements to the doorway in front of you — which is exactly what the tradesperson does, and exactly what the room is watching for.