Cruxly › Low-level design › Principles & Patterns
The Patterns That Actually Recur
Strategy, Factory, Observer, Decorator, Adapter — motivated, not memorised
Open the interactive lessonThe Gang-of-Four catalogue has 23 patterns. LLD interviews draw from about six of them, repeatedly, because those six are the ones SOLID violations most often resolve into.
This lesson is deliberately not a reference card. Each pattern is introduced by the problem that forces it — because a pattern named before its problem is decoration, and the interview is checking for the problem-recognition, not the vocabulary.
Strategy — behaviour that varies and must swap at runtime
Forcing problem: FeeStrategy, FitPolicy, Scheduler all have this shape — an algorithm that the prompt has told you will vary, needs to be selected per-context, and must be swappable without touching the class that uses it.
interface FeeStrategy { Fee compute(Ticket t); }
class HourlyFee implements FeeStrategy { ... }
class FlatFee implements FeeStrategy { ... }
class Ticket {
FeeStrategy pricing; // held, not hardcoded
Fee finalFee() { return pricing.compute(this); }
}
Every exercise in this track that named an interface for "the thing that varies" — pricing, fitting, scheduling — was independently arriving at Strategy. The pattern is just the name for that recurring shape.
Factory Method — construction that depends on runtime information
Forcing problem: you need to create one of several related types, and which one depends on something not known until runtime — and you do not want every call site running its own if/else on that decision.
// every call site repeats this decision
Piece p = type == KING ? new King() : type == QUEEN ? new Queen() : ...;
class PieceFactory {
static Piece create(PieceType type, Colour c) {
switch (type) {
case KING: return new King(c);
case QUEEN: return new Queen(c);
// ...
}
throw new IllegalArgumentException();
}
}
One switch, one place. This looks like it contradicts Open/Closed — isn't a switch exactly what OCP says to avoid? The honest answer: construction logic is the one place a type-switch is usually acceptable, because a factory's entire job is to know about every concrete type. The chess exercise elsewhere in this track keeps piece geometry behind an interface precisely so this factory is the only place that names every concrete piece type — everywhere else programs against Piece.
Observer — one change, several unrelated things need to react
Forcing problem: when X happens, several independent parts of the system need to know, and X should not have to hardcode a call to each one — new subscribers should be addable without editing X.
interface TicketListener { void onIssued(Ticket t); }
class ParkingLot {
List<TicketListener> listeners = new ArrayList<>();
void subscribe(TicketListener l) { listeners.add(l); }
Ticket admit(Vehicle v) {
Ticket t = ...;
listeners.forEach(l -> l.onIssued(t)); // notify, without knowing who
return t;
}
}
A display board, an analytics logger, and a capacity alert can all subscribe independently. ParkingLot.admit never changes when a fourth subscriber is added — which is Open/Closed again, arrived at through Observer.
:::key The distributed-systems version of this pattern Message queues and pub/sub systems are Observer at a different scale — a publisher that does not know or care who is subscribed. Recognising that a system-design "decouple producers from consumers with a queue" question and an LLD "notify several listeners" question are the same underlying shape, just at different scales, is a genuinely strong thing to say in an interview. :::
Decorator — adding behaviour without touching the original class or exploding subclasses
Forcing problem: you need to layer optional behaviour onto an object, and the combinations would explode a subclass hierarchy — ElectricCompactCar, ElectricLargeCar, HybridCompactCar... one subclass per combination.
interface FeeStrategy { Fee compute(Ticket t); }
class HourlyFee implements FeeStrategy {
public Fee compute(Ticket t) { return new Fee(hours(t) * rate); }
}
class WeekendSurcharge implements FeeStrategy { // wraps another strategy
FeeStrategy inner;
WeekendSurcharge(FeeStrategy inner) { this.inner = inner; }
public Fee compute(Ticket t) {
Fee base = inner.compute(t);
return isWeekend(t) ? base.times(1.5) : base;
}
}
FeeStrategy pricing = new WeekendSurcharge(new HourlyFee()); // composed, not subclassed
This is exactly ElectricVehicleFee from the Open/Closed lesson, wrapping a base FeeStrategy — Decorator is the name for that wrapping shape. Any number of surcharges can compose without a combinatorial subclass explosion, because each one only needs to know about the interface, not about each other.
Adapter — making an interface you don't control fit one you do
Forcing problem: you need to use a class whose interface doesn't match what your code expects — usually a third-party library or legacy code you cannot modify.
// third-party, fixed shape you cannot change
class LegacyPaymentGateway {
int chargeInCents(String cardToken, long cents) { ... }
}
// the interface YOUR code was designed against
interface PaymentProcessor { void charge(Money amount, String token); }
class LegacyGatewayAdapter implements PaymentProcessor {
LegacyPaymentGateway legacy;
public void charge(Money amount, String token) {
legacy.chargeInCents(token, amount.toCents()); // translates the call
}
}
The adapter is the entire cost of integrating a mismatched interface, isolated to one class. Every other class in the system programs against PaymentProcessor and never learns the legacy gateway's shape exists. This is the pattern to name when an interviewer says "now integrate with vendor X's SDK" — it is almost always an Adapter, not a redesign.
How to actually use this list in an interview
Do not open a design by picking a pattern. Open by stating the problem — "pricing needs to vary," "several things need to react to one event," "I need optional add-on behaviour without a subclass explosion" — and let the pattern name arrive as a description of the class shape you already derived.
If a pattern name is the first thing out of your mouth, the interviewer cannot tell whether you understood the problem or memorised a solution looking for one. If the problem statement comes first and the pattern name is how you label what you just built, there is no ambiguity.
How to say it in the interview
"I don't reach for a pattern by name. I notice the shape of the problem — 'behaviour needs to vary and swap at runtime,' say — and Strategy is just the name for the class shape that falls out of solving it."
Checklist
- Recognised Strategy from 'behaviour varies + must swap at runtime', not from the name
- Used Factory Method and named why a switch there doesn't contradict Open/Closed
- Used Observer for one-event-many-reactors and connected it to pub/sub at system-design scale
- Used Decorator to add optional behaviour by wrapping rather than subclassing
- Used Adapter to isolate a third-party interface mismatch to one class
- Named the problem before naming the pattern, in that order, out loud