Adapting to changing requirements
Multi-level rounds grade how well your level 1 survives levels 2 to 4. Build level 1 simply, but make three cheap bets: store records, not bare values; accept the inputs that will obviously arrive (time, ids); and change each piece of state in exactly one place. When a new level doesn’t fit, refactor first, then add the feature, with the old tests as your safety net.
The idea in plain words
Section titled “The idea in plain words”Many practical rounds aren’t one puzzle. They are one small system that grows: a store, a ledger, a scheduler. You get level 1, make it pass, then level 2 arrives and changes the rules. CodeSignal’s widely used Industry Coding Framework makes this explicit: one project, four levels, 90 minutes. Its published rubric says level 1 checks basic implementation and corner cases, level 2 reusing level 1 code, level 3 keeping backward compatibility while extending, and level 4 adjusting earlier functionality without regressions, with code design judged after the fact. Live interviews at Stripe, Anthropic and others follow the same shape with an interviewer instead of a test runner.
So the real test is not “can you write level 1”. It’s “how much of level 1 do you have to rewrite at level 3?”
Think of building a house you know will get a second floor. You don’t build the second floor now. But you pour a foundation that can take the weight, and you leave the stairwell where it will go. That costs a little extra on day one and saves tearing the house down later.
The counterweight is YAGNI (“you aren’t gonna need it”): don’t build features nobody asked for. The skill is telling a cheap, likely bet (a field, a parameter, one helper) from speculative machinery (plugin systems, abstract base classes, generic event buses).
See it
Section titled “See it”The same invented problem, a library that lends books, built two ways. The left design is the minimum that passes level 1. The right one makes three small bets. Step through the levels and watch which parts each new requirement touches.
+ free = Counter() + out = set() # (member, isbn) + checkout / return_book flip one set entry
+ @dataclass Loan(member, isbn, out_day)
+ loans = {} # (member, isbn) -> Loan
+ methods take day (ignored for now)The minimal design wins at level 1, ties at level 2, and loses badly at level 3, the level where time arrives. That’s typical: level 3 is where the shortcuts of level 1 get billed.
When to reach for it
Section titled “When to reach for it”| The problem says… | The likely next level… | The cheap bet now |
|---|---|---|
| “implement a store / system with operations…” | queries over the same data (rankings, filters, totals) | Keep data in records keyed by id, so queries can scan or index them. |
operations take a timestamp, even if unused |
expiry, history, “as of time t”, scheduling | Thread the timestamp through every method from the start. |
| “return true/false if it succeeded” | return why, or a value (fee, balance, remaining quota) | Funnel results through one place, so widening the return type is one edit. |
| “a user can…” | per-user limits, merges, permissions | Key state by user id; don’t fold users into totals too early. |
| “add X to Y” | undo, transfers, partial failures | One method per state change, so it can be logged or reversed. |
| a numeric total | money, rounding, currencies | Integers in minor units from day one. |
How it works
Section titled “How it works”- Read level 1 for its nouns and verbs. Nouns become records or classes (
Loan,Account); verbs become methods. Ask the interviewer: “Will there be more levels? Is there a notion of time?” They will often hint. - Make three cheap bets. Records instead of bare ints. The obvious extra inputs (timestamp, ids) already in the signatures. Each piece of state mutated in exactly one place.
- Write level 1 tests as a table and keep them. They are the regression suite for every later level.
- When a new level arrives, classify the change. Is it an addition (new method, new field, new index), an extension (an existing method does a bit more), or a change of model (the core data shape is wrong)?
- Additions and extensions: add, then run all tests. Put the new logic where the related mutation already happens.
- Change of model: refactor first, keeping the old tests green, then add the feature. Kent Beck’s line: “make the change easy (this may be hard), then make the easy change.”
- Say it out loud. “Level 3 needs due dates per loan. My loans are records, so this is one field and two lines.” Interviewers grade the reasoning as much as the diff.
The worked example in code
Section titled “The worked example in code”Here is the “room to grow” library after three levels. The comments mark which level added each line. Notice how little level 2 and level 3 needed: one index and one counter line for level 2, one field and two lines for level 3. The records and timestamps were already there.
from collections import Counterfrom dataclasses import dataclass
LOAN_DAYS = 14 # L3FEE_PER_DAY_CENTS = 25 # L3: money as integer cents, never floats
@dataclassclass Loan: # L1: a record, not a bare int, so later levels add fields, not structures member: str isbn: str out_day: int # L1 already accepts a time, even though level 1 ignores it due_day: int # L3
class Library: def __init__(self) -> None: self.free: Counter[str] = Counter() # L1: isbn -> copies on the shelf self.loans: dict[tuple[str, str], Loan] = {} # L1: (member, isbn) -> open loan self.times_borrowed: Counter[str] = Counter() # L2: one extra index, updated in one place
def add_copies(self, day: int, isbn: str, n: int) -> None: if n <= 0: raise ValueError("n must be positive") self.free[isbn] += n
def checkout(self, day: int, member: str, isbn: str) -> bool: if self.free[isbn] == 0 or (member, isbn) in self.loans: return False self.free[isbn] -= 1 self.loans[(member, isbn)] = Loan(member, isbn, day, day + LOAN_DAYS) self.times_borrowed[isbn] += 1 # L2: the only line level 2 added here return True
def return_book(self, day: int, member: str, isbn: str) -> int | None: loan = self.loans.pop((member, isbn), None) if loan is None: return None # L1 returned False; L3 widened it to "fee or None" self.free[isbn] += 1 return max(0, day - loan.due_day) * FEE_PER_DAY_CENTS # L3
def top_borrowed(self, k: int) -> list[str]: # L2: ties broken by isbn, stated up front ranked = sorted(self.times_borrowed.items(), key=lambda kv: (-kv[1], kv[0])) return [f"{isbn}({n})" for isbn, n in ranked[:k]]
if __name__ == "__main__": lib = Library() lib.add_copies(0, "dune", 1); lib.add_copies(0, "emma", 2) assert lib.checkout(1, "ana", "dune") and not lib.checkout(1, "bo", "dune") # L1: no copies left assert lib.checkout(2, "bo", "emma") and lib.checkout(2, "cy", "emma") assert lib.top_borrowed(5) == ["emma(2)", "dune(1)"] # L2 assert lib.return_book(15, "ana", "dune") == 0 # L3: on the due day assert lib.return_book(20, "bo", "emma") == 4 * 25 # L3: 4 days late assert lib.return_book(20, "bo", "emma") is None # L1: double return print("ok")import assert from "node:assert/strict";
const LOAN_DAYS = 14; // L3const FEE_PER_DAY_CENTS = 25; // L3: money as integer cents, never floats
// L1: a record, not a bare number, so later levels add fields instead of new structures.interface Loan { member: string; isbn: string; outDay: number; dueDay: number /* L3 */ }
export class Library { private free = new Map<string, number>(); // L1: isbn -> copies on the shelf private loans = new Map<string, Loan>(); // L1: key `${member}|${isbn}` (arrays are compared by reference) private timesBorrowed = new Map<string, number>(); // L2: one extra index, updated in one place private static key = (member: string, isbn: string) => `${member}|${isbn}`;
addCopies(day: number, isbn: string, n: number): void { // L1 already takes a time it ignores if (n <= 0) throw new RangeError("n must be positive"); this.free.set(isbn, (this.free.get(isbn) ?? 0) + n); }
checkout(day: number, member: string, isbn: string): boolean { const k = Library.key(member, isbn); if ((this.free.get(isbn) ?? 0) === 0 || this.loans.has(k)) return false; this.free.set(isbn, this.free.get(isbn)! - 1); this.loans.set(k, { member, isbn, outDay: day, dueDay: day + LOAN_DAYS }); this.timesBorrowed.set(isbn, (this.timesBorrowed.get(isbn) ?? 0) + 1); // L2: the only line added here return true; }
returnBook(day: number, member: string, isbn: string): number | null { const k = Library.key(member, isbn); const loan = this.loans.get(k); if (!loan) return null; // L1 returned false; L3 widened it to "fee or null" this.loans.delete(k); this.free.set(isbn, (this.free.get(isbn) ?? 0) + 1); return Math.max(0, day - loan.dueDay) * FEE_PER_DAY_CENTS; // L3 }
topBorrowed(k: number): string[] { // L2: ties broken by isbn, stated up front return [...this.timesBorrowed] .sort(([a, x], [b, y]) => y - x || (a < b ? -1 : a > b ? 1 : 0)) .slice(0, k) .map(([isbn, n]) => `${isbn}(${n})`); }}
const lib = new Library();lib.addCopies(0, "dune", 1); lib.addCopies(0, "emma", 2);assert.ok(lib.checkout(1, "ana", "dune") && !lib.checkout(1, "bo", "dune")); // L1: no copies leftassert.ok(lib.checkout(2, "bo", "emma") && lib.checkout(2, "cy", "emma"));assert.deepEqual(lib.topBorrowed(5), ["emma(2)", "dune(1)"]); // L2assert.equal(lib.returnBook(15, "ana", "dune"), 0); // L3: on the due dayassert.equal(lib.returnBook(20, "bo", "emma"), 4 * 25); // L3: 4 days lateassert.equal(lib.returnBook(20, "bo", "emma"), null); // L1: double returnconsole.log("ok");var lib = new Library();lib.AddCopies(0, "dune", 1); lib.AddCopies(0, "emma", 2);Check(lib.Checkout(1, "ana", "dune") && !lib.Checkout(1, "bo", "dune"), "L1: no copies left");Check(lib.Checkout(2, "bo", "emma") && lib.Checkout(2, "cy", "emma"), "L1: two copies");Check(lib.TopBorrowed(5).SequenceEqual(["emma(2)", "dune(1)"]), "L2: ranking");Check(lib.ReturnBook(15, "ana", "dune") == 0, "L3: on the due day");Check(lib.ReturnBook(20, "bo", "emma") == 4 * 25, "L3: 4 days late");Check(lib.ReturnBook(20, "bo", "emma") is null, "L1: double return");Console.WriteLine("ok");
static void Check(bool ok, string name) { if (!ok) throw new Exception($"failed: {name}"); }
// L1: a record, not a bare int, so later levels add fields instead of new structures.record Loan(string Member, string Isbn, int OutDay, int DueDay /* L3 */);
class Library{ const int LoanDays = 14, FeePerDayCents = 25; // L3: money as integer cents readonly Dictionary<string, int> free = []; // L1: isbn -> copies on the shelf readonly Dictionary<(string Member, string Isbn), Loan> loans = []; // L1: value tuples hash by value readonly Dictionary<string, int> timesBorrowed = []; // L2: one extra index
public void AddCopies(int day, string isbn, int n) // L1 already takes a time { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(n); free[isbn] = free.GetValueOrDefault(isbn) + n; }
public bool Checkout(int day, string member, string isbn) { if (free.GetValueOrDefault(isbn) == 0 || loans.ContainsKey((member, isbn))) return false; free[isbn]--; loans[(member, isbn)] = new Loan(member, isbn, day, day + LoanDays); timesBorrowed[isbn] = timesBorrowed.GetValueOrDefault(isbn) + 1; // L2: the only line added here return true; }
public int? ReturnBook(int day, string member, string isbn) // L1 bool -> L3 "fee or null" { if (!loans.Remove((member, isbn), out var loan)) return null; free[isbn]++; return Math.Max(0, day - loan.DueDay) * FeePerDayCents; // L3 }
public List<string> TopBorrowed(int k) => // L2: ties broken by isbn [.. timesBorrowed.OrderByDescending(kv => kv.Value).ThenBy(kv => kv.Key, StringComparer.Ordinal) .Take(k).Select(kv => $"{kv.Key}({kv.Value})")];}Refactor or patch?
Section titled “Refactor or patch?”When level N doesn’t fit, you have two moves:
| Patch | Refactor, then add | |
|---|---|---|
| What it is | Bolt the feature on beside the old model (a second dict, a special case) | Change the model so the feature is natural, keeping old tests green |
| Good when | The change is local and the next level is unlikely to touch it again | The same data now has two homes, or you’d special-case in several places |
| Risk | Two sources of truth drift apart; every later level pays again | Time spent; a broken refactor with no tests |
| Tell-tale sign you need to refactor | You write the same lookup in three methods, or you keep two structures in sync by hand |
A useful rule: patch once, refactor on the second patch to the same area. And never refactor without tests running.
What it costs
Section titled “What it costs”| Strategy | Level 1 | Levels 2-4 | Risk |
|---|---|---|---|
| Minimal, patch as you go | fastest | slower each level; often a rewrite at 3 or 4 | regressions, two sources of truth |
| Cheap bets (records, timestamps, one mutation point) | +5-10% | mostly additions | slight over-design if the next levels go elsewhere |
| Speculative framework (interfaces, plugins, events) | much slower | may help, may not | runs out of time before level 2 |
In a 90-minute, four-level format, the middle row is the sweet spot: the bets are a few lines each, and level 3 is where most candidates run out of time.
Variations you’ll meet
Section titled “Variations you’ll meet”- Hidden levels. On test-runner platforms, you may only see level N+1 after passing level N, so you design blind. The cheap bets are your hedge.
- Live follow-ups. With an interviewer, the next level is a conversation: “Now make it work when two calls race” or “What if this has to be distributed?” Answer the design first, then code the smallest change.
- Backward compatibility. Level 3 may change a return type (“return the fee instead of true”). Keep old callers working: widen carefully, or add a new method beside the old one.
- Changing an earlier rule. Level 4 sometimes overrides a level 1 rule (“a returned copy now goes to the hold queue”). Find the one place that rule lives. If it lives in three places, refactor first.
Pitfalls
Section titled “Pitfalls”- Over-engineering level 1. An abstract repository and a strategy pattern before the first test passes. Make bets, not frameworks.
- Bare primitives as state.
dict[str, int]of counts can’t grow into “count plus due date plus history” without a rewrite. - Dropping the timestamp. If the signature has
timestampand level 1 ignores it, keep it anyway and store it. Time almost always matters later. - Duplicated mutation. Updating a balance in
deposit,transferandpayseparately means three places to change when fees arrive. - Deleting old tests to make new ones pass. A level that breaks level 1 tests usually means a regression, not an obsolete test. Check the prompt.
- Silent behaviour changes. If level 3 changes what an old method returns, say so and update its tests deliberately.
In the interview
Section titled “In the interview”- At the start: “Is this going to be extended? Is there a notion of time or users I should plan for?” It’s a fair question, and the answer shapes your data model.
- Name your bets briefly: “I’ll store loans as records keyed by member and book. It costs nothing now and makes history easy later.”
- When the next level lands, classify it out loud: addition, extension, or change of model.
- If you refactor, say why and keep it small: “Both rankings and fees need per-loan data, so I’ll move from a set to a dict of records first, rerun the tests, then add fees.”
- Expect the closing question “What would you change if you had more time?” Have one honest answer about the design.
Check yourself
Section titled “Check yourself”Practice on your own
Section titled “Practice on your own”Build each in four levels. Write level 1 without looking ahead, then score how much level 3 forced you to rewrite:
- Parking garage: park/leave → occupancy by floor → hourly pricing with timestamps → reservations that expire.
- Gym front desk: check members in and out → busiest hour of the day → memberships that expire → guest passes with a visit history.
- Plant watering log: register plants, record waterings → overdue plants → per-species schedules → watering history with undo.
- Bike rental dock: rent and return bikes → most-ridden bikes → pricing by ride duration → a waiting list when the dock is empty.
Go deeper
Section titled “Go deeper”- CodeSignal: Industry Coding Framework datasheet: the published four-level rubric.
- Martin Fowler, Refactoring (2nd ed., 2018), and “An example of preparatory refactoring”: “make the change easy, then make the easy change”.
- Martin Fowler, “Yagni”: the cost of building features before you need them, and why small refactorable code is the answer.
- Sandi Metz, Practical Object-Oriented Design (2nd ed.), ch. 2-3: single responsibility and managing dependencies so change stays local.