Syllabus · F4
Decision defence and trade-off articulation
- Tier 3
- Process
- 5 min read
The idea
Decision defence — articulating the choice you made
In an interview, you say something like "I put the algorithm behind an interface," or "I held the lock across both checks," or "I use a double, not a fraction." The interviewer's next move is almost always the same question: "why not do it the other way?" That question is the actual interview, underneath whatever design you were describing. A decision you cannot defend against it is not a decision. It is a guess dressed up as one, and the dressing comes off the moment someone asks why.
A real defence has three parts, and each one does a job the other two cannot do for it. First, name the alternative you rejected: "I could have put the pricing logic inside the limiter itself, instead of behind a separate interface." Second, name the cost of that alternative: "With the logic inside, adding a third pricing rule would mean editing the limiter's code in more than one place." Third, say why you chose your way instead: "By putting each rule behind an interface, a new one is a new class, with zero edits to any code that already exists." Skip any one of those three parts and what you have said is a preference, stated with confidence. It is not yet a defended decision.
The difference sounds small until you hear both versions back to back. "I put the algorithm behind an interface because it's cleaner" names no alternative and no cost, so an interviewer has nothing to probe except your taste. Compare it to this instead: "I put the algorithm behind an interface rather than as a branch inside the limiter. A branch means editing the limiter every time a new rule is added; an interface means adding a class instead." That second version survives the follow-up question, because the follow-up has already been answered inside it.
You will hear this question again, later, in the form of a curveball: a requirement the interviewer adds that was never part of the scope you negotiated earlier in the round. If you made the right structural choices going in, the new requirement lands cleanly behind a seam that already exists. That costs one file, maybe ten lines, because the seam already named the axis the requirement was going to move along, even before you knew this specific requirement was coming.
Skip the trade-off analysis up front, and the very same requirement turns into a retrofit instead. Thirty lines, spread across three files. Every place in the code that quietly assumed there was only one algorithm now has to be found by hand and edited one at a time. The design did not become more complicated when the requirement arrived. It only revealed complexity that had been sitting there unexamined the whole time, waiting for a second case to expose it.
The threshold for when this matters is simple to state, even if it takes practice to do under a clock. Before you write a line of code, name one thing you could have done instead, and say plainly why you did not do it that way. That is not overthinking, and it is not a ritual to perform for the interviewer's benefit. It is the entire skill this lesson is about.
Worked walkthrough
Decision Defence: Reading the Reference Solution
Run it first
Find these reference implementations and look for how decisions are documented:
corpus/rate-limiter/reference/src/
corpus/cost-explorer/reference/src/
corpus/parking-lot/reference/src/
In each one, find comments or documentation that explain why a design choice was made, not only what it does.
What a decision defence looks like
A strong defence has this shape, written as the interviewer would ask it:
Question: "Why hold a ReentrantLock per key instead of one global lock?"
Decision: One lock per key, not a global lock.
What it anticipates: If the limiter becomes a bottleneck, a global lock makes every request wait for every other request to finish, even if they're different clients. Separate locks let two different keys run at the same time.
Cost without it: A global lock is simpler to write — no risk of deadlock, easier to reason about atomicity. But under load, throughput drops to zero. One request waits for every other one, and in a high-concurrency scenario, that is exactly the problem the rate limiter was installed to prevent.
Real example: Rate Limiter
In rate-limiter/reference/src, the decision about Algorithm and AlgorithmFactory:
Question: "Why is there an AlgorithmFactory at all? Why not construct the right algorithm directly in the rule?"
Decision: AlgorithmFactory is a separate seam from RateLimitAlgorithm. The factory decides which class implements which algorithm; the interface decides how that class behaves.
What it anticipates: When the interviewer asks "add a sliding-window algorithm," the answer is one new class plus one line in the factory — the registration line. Without the factory, adding a new algorithm means editing KeyBudget, the place where rules meet locks. That's the class with the most invariants to break.
Cost without it: It's tempting to put the algorithm selection inside KeyBudget as a switch statement. That works for two algorithms. For a third, you edit the switch, retest the whole class, and risk changing the locking logic by accident. The factory-as-data approach uses the rule's constant to look up its implementation; a new algorithm is a new registration line, never an edit to the limiter's core logic.
Another angle: Parking Lot
In parking-lot, the decision about storing spots in a flat array:
Question: "Why not a tree or a hash of levels, since a lot can have multiple floors?"
Decision: All spots in one flat array. No nesting, no per-level structure.
What it anticipates: A parking lot in the first version is one floor. You can query by spot number, add and remove spots, but there's no vertical nesting. Simplicity is the win.
Cost without it: If you build a tree structure for multi-floor support from the start, you're paying the cost of traversal, level-specific logic, and per-floor checking before you've heard "curveball." The flat array works perfectly for one floor. If the interviewer adds floors later, that's when you refactor. You've already shown that handling what the problem actually asks beats speculating on what it might ask.
The pattern
Every decision defence you write has this structure:
- The question — what the interviewer asks, or what you ask yourself at the whiteboard
- The decision — what you chose
- The anticipation — what problem this choice solves or what future change it makes easier
- The cost — what you gave up by choosing this way, and what you would lose without it
The entries that carry the most weight are the ones that defend a refusal. Why no concurrency in parking lot? Why no persistence? Why no metrics in rate-limiter? A decision to do something is easier to defend than a decision not to. The "why not" is what separates a candidate who thought about trade-offs from one who coded the happy path only.
How to practise
Read the reference solution for a problem. Find one architectural choice — an interface, a data structure, a separation of concerns. Ask yourself: "Why this way and not the obvious alternative?" Write down the question, the decision, what it anticipates, and what it would cost without it. Then check if the reference's own comments or documentation backs up your reasoning.
The goal is not to memorize answers, but to practise the shape of the question. When the interviewer says "add metrics to the rate limiter," you should not panic. You should say: "Metrics are out of scope as we defined it. Adding them without a seam means retrofitting logging into the decision logic here — that's 20 lines across multiple places. If metrics were in scope from the start, we'd have a separate interface, and this costs one new file."
That's not defended from a memorized answer. It's defended from understanding the trade-off you made at the whiteboard.
Worked source
The 1 file of the worked design
Every file below is the one the app opens, verbatim. This is the part worth reading slowly: the prose above argues for a shape, and these are the lines that have it.
worked/Main.java133 lines
worked/Main.java133 lines
/**
* Decision Defence: Real Examples from LLD Interviews
*
* This program walks through how to articulate decisions at the whiteboard.
* See NOTES.md for the full explanation and how to read these from reference solutions.
*/
public class Main {
static class DecisionDefence {
String question;
String decision;
String anticipates;
String cost;
DecisionDefence(String q, String d, String a, String c) {
this.question = q;
this.decision = d;
this.anticipates = a;
this.cost = c;
}
void print() {
System.out.println("\nQ: " + question);
System.out.println("A: " + decision);
System.out.println(" Anticipates: " + anticipates);
System.out.println(" Cost: " + cost);
}
}
public static void main(String[] args) {
System.out.println("Decision Defence: Articulating Trade-offs");
System.out.println("=".repeat(70));
System.out.println("\nThree real decisions from the corpus, with their defences:");
new DecisionDefence(
"Why is the rate limiter's Algorithm behind an interface?",
"Each counting algorithm (fixed window, token bucket) is a separate class",
"When the interviewer asks for a third algorithm, we add a new class and one " +
"registration line. The limiter's logic is untouched.",
"Without the interface, adding a third algorithm means editing the class that " +
"holds the lock and the all-or-nothing commit. That's the highest-risk place in " +
"the code to edit."
).print();
new DecisionDefence(
"Why hold one lock per rate limiter key, not a global lock?",
"Each key's budget gets its own ReentrantLock. Threads on different keys " +
"don't contend.",
"If one key gets hammered with requests, it doesn't block another key's traffic. " +
"You're not replacing the problem you were hired to fix (overload) with a " +
"different one (lock contention).",
"A global lock is simpler to write and easier to reason about. Under load, " +
"every request waits for every other request. The limiter becomes the bottleneck."
).print();
new DecisionDefence(
"Why does Cost Explorer use integer arithmetic for money, not doubles?",
"Money is stored as minor units (cents): a long, never a double.",
"Proration divides a monthly price by the day count of that month. With a double, " +
"one-third of a cent is inexact. The remainder is lost to rounding. With integers, " +
"the remainder is visible and the requirement 'customer absorbs the remainder' " +
"becomes mechanical.",
"Doubles would be shorter to write. You'd avoid rounding logic. But you'd lose " +
"the ability to enforce a stated rounding convention, and you'd discover the bug " +
"in production when a customer's refund is off by a penny."
).print();
System.out.println("\n" + "=".repeat(70));
System.out.println("The Pattern Every Defence Follows");
System.out.println("=".repeat(70));
System.out.println("\n1. The interviewer asks a question: 'Why did you do X?'");
System.out.println(" or 'Why not do Y instead?'");
System.out.println("\n2. You answer with a decision: 'I chose to [do X], because'");
System.out.println(" [state the problem it solves].");
System.out.println("\n3. You explain what it anticipates:");
System.out.println(" 'This matters when [curveball arrives]. The new requirement");
System.out.println(" lands cleanly because [seam already exists].'");
System.out.println("\n4. You name the cost of not choosing it:");
System.out.println(" 'Without this decision, I'd be [retrofitting / rewriting /");
System.out.println(" editing the most fragile class]. That costs [time / risk / lines].'");
System.out.println("\n" + "=".repeat(70));
System.out.println("How to Practise");
System.out.println("=".repeat(70));
System.out.println("\nRead a reference solution.");
System.out.println("Find one architectural choice: an interface, a data structure,");
System.out.println(" a separation of concerns.");
System.out.println("Ask yourself: 'Why this way, not the obvious alternative?'");
System.out.println("Write the four parts of the defence.");
System.out.println("Then check if the reference's own comments back up your answer.");
System.out.println("\n" + "=".repeat(70));
System.out.println("When It Matters Most");
System.out.println("=".repeat(70));
System.out.println("\nThe entries that carry the most weight defend a REFUSAL.");
System.out.println("- Why NO concurrency in parking lot?");
System.out.println("- Why NO persistence?");
System.out.println("- Why NO metrics in rate-limiter?");
System.out.println("\nA decision to do something is easier to defend than a");
System.out.println("decision NOT to. The 'why not' is what separates a candidate");
System.out.println("who thought about trade-offs from one who coded the happy path.");
System.out.println("\n" + "=".repeat(70));
System.out.println("The Interview Test");
System.out.println("=".repeat(70));
System.out.println("\nThe interviewer adds a curveball.");
System.out.println("\nScenario A (you anticipated it):");
System.out.println(" Curveball: 'Add metrics to the rate limiter.'");
System.out.println(" Your answer: 'Metrics are out of scope. But I put each rule");
System.out.println(" behind an interface, so this is a new decorator on");
System.out.println(" RateLimitAlgorithm. One new file, zero edits.'");
System.out.println(" Verdict: You're prepared. That's extensibility.");
System.out.println("\nScenario B (you didn't anticipate it):");
System.out.println(" Curveball: 'Add metrics.'");
System.out.println(" Your answer: (silence, or) 'Let me add a counter to the");
System.out.println(" decision logic...' (now editing RateLimiter, three files,");
System.out.println(" touching the lock, risking the invariant)");
System.out.println(" Verdict: You're retrofitting. That's fragile.");
System.out.println("\nBoth candidates can add the feature. The difference is");
System.out.println("whether they anticipated the cost in their design.");
}
}
The faded stage is not here, on purpose
In the app, the third stage of this lesson is a written completion: a prompt with blanks, answered in prose and then graded against what the reference extraction expects. The grading needs the app, so this page stops at the worked walkthrough.
Run the app for the drill: it is the download in the header, and it works offline once unpacked.