Back to the path
Level 6 expert 26 min #lld#machine-coding#interview-strategy#design-process#trade-offs#senior

The Machine-Coding Round: A Senior Playbook

A concrete 60-90 minute playbook for the machine-coding / LLD interview round at Senior level: how to scope requirements, drive the design top-down, choose patterns deliberately, write clean compiling code, absorb extensibility curveballs, manage your time budget, and defend trade-offs out loud — plus what separates a hire from a strong-hire.

Why this round is different from everything else you’ve practiced

By now you can apply SOLID, recognize when a Strategy beats a switch, and model a parking lot in your sleep. The machine-coding round does not test whether you know these things — it tests whether you can run a controlled design process under a clock, out loud, with an interviewer changing the requirements on you. The artifact (your code) matters, but the round is really an audit of your judgment: what you choose to build, what you deliberately defer, and whether you can say why.

This lesson is a playbook, not a problem. It gives you a phase-by-phase script, a time budget, concrete good-vs-weak moves, and the exact sentences that move you from hire to strong-hire.

The single biggest scoring lever at Senior level is legibility of reasoning. Two candidates can write the same class Order. The one who said “I’m making PricingStrategy an interface now because discounts and taxes are the most likely change axis” out-scores the one who silently typed it — even if the code is byte-identical.

Key idea

The interviewer is building a mental model: “Would I trust this person to own a service and review my juniors’ PRs?” Every move should answer yes. Narrate intent, scope explicitly, keep the code compiling, and defend trade-offs as trade-offs — not as universally correct choices.

The time budget (90-minute round)

Treat the clock as a first-class constraint. A working, slightly-narrow solution beats a beautiful half-finished one every time. Reserve the back end for a working demo.

PhaseTimeGoalFailure mode if you skip it
1. Clarify & scope5-10 minLock the problem, write down in/out of scopeYou build the wrong thing or boil the ocean
2. Top-down design10-15 minCore entities, key interfaces, one diagramBottom-up mud; no extensibility story
3. Core implementation35-45 minHappy path compiling + the 1-2 hard partsLots of stubs, nothing runs
4. Demo / driver5-10 minmain() that exercises the flow, prints output”It would work” — unproven
5. Extension volley5-15 minAbsorb the curveball, show the seamYou rewrite instead of extending
6. Wrap & defendlast 5 minList trade-offs, what you’d do with more timeLooks like you didn’t notice the gaps
Tip

Say the budget out loud at minute zero: “I’ll spend ~10 min scoping, ~15 on the skeleton, then code the core and leave time for a runnable demo.” This signals seniority before you’ve written a line — you’re managing the round, not surviving it.

Phase 1 — Clarify and scope (the highest-leverage 10 minutes)

Vague prompts (“Design a ride-sharing system”) are intentional. Scoping is the test. Your job is to convert an open prompt into a bounded, demonstrable problem and get the interviewer to nod.

Drive it along these axes, fast:

  • Actors & core use cases — who uses it, what are the 2-3 verbs? (“Riders request, drivers accept, system matches and prices.”)
  • Functional MUST vs NICE — propose the cut yourself, don’t ask permission for everything.
  • Scale & concurrency — single-process in-memory? Or do they want thread-safety modeled? This decides whether you reach for mutex/locks.
  • Persistence — almost always say: “I’ll use in-memory repositories behind an interface so storage is swappable; no real DB.” This is the correct default and shows you know the seam.
  • The likely change axisask this explicitly: “What’s most likely to change later — pricing rules, matching strategy, vehicle types?” Their answer tells you where to spend your design budget.

Weak move: “Should I handle payments?” → “Up to you.” (You offloaded the decision.) Strong move: “Payments touch external gateways and add little design signal, so I’ll model a PaymentService interface and stub it — in scope as an interface, out of scope as an implementation. Sound good?”

Then write the scope down (top of the file or a corner of the board) as a contract:

IN  : request ride, match nearest driver, surge pricing, ride lifecycle
OUT : auth, real payments, persistence, maps/routing (stub distance)
ASSUME: single process, in-memory, ~thousands of drivers
Watch out

Do not start coding until the scope is acknowledged. An interviewer who lets you sprint off in the wrong direction is collecting evidence, not helping you. The five minutes you “save” by skipping confirmation is the most expensive five minutes in the round.

Phase 2 — Drive the design top-down

Now externalize a model before implementing. You want a small diagram and a short list of interfaces at the change axes you identified in Phase 1. Top-down means: nouns → responsibilities → relationships → the 1-2 interfaces that carry your extensibility story. Resist jumping to a clever pattern; let the requirement summon it.

        +-----------+      requests      +------------------+
 Rider  |   Rider   |------------------->|   RideService    |
        +-----------+                    | (orchestrator)   |
                                         +--------+---------+
                                                  | uses
                  +-------------------------------+------------------+
                  |                  |                   |           |
            +-----v------+   +-------v--------+   +-------v------+   +v---------+
            | Matching   |   | Pricing        |   | Driver       |   | Ride     |
            | Strategy   |   | Strategy       |   | Repository   |   | (entity, |
            | <<iface>>  |   | <<iface>>      |   | <<iface>>    |   |  FSM)    |
            +-----+------+   +-------+--------+   +--------------+   +----------+
                  |                  |
           NearestDriver        SurgePricing
           (impl)               (impl)

State machines are gold when the entity has a lifecycle — model Ride as an explicit FSM and say it’s a state machine; it pre-empts a whole class of “what if a cancelled ride gets accepted?” questions:

REQUESTED --accept--> MATCHED --start--> ONGOING --end--> COMPLETED
    |                    |
 cancel               cancel
    v                    v
CANCELLED            CANCELLED

Choose patterns deliberately, and say the alternative you rejected

Pattern-dropping for its own sake reads as junior. The senior signal is naming the alternative you didn’t pick.

Requirement signalReach forSay out loud
”pricing/matching rules will change”Strategy”Strategy over a flag enum so a new rule is a new class, not a switch edit"
"notify multiple subscribers on event”Observer”Observer so adding an SMS channel doesn’t touch the publisher"
"complex object built step by step”Builder”Builder because the constructor has 6 optional fields"
"create families of related objects”Factory”A factory centralizes the new, so the orchestrator depends on interfaces"
"undo / queued ops”Command”Command to make operations first-class and reversible”
Common pitfall

Over-engineering is a negative signal, not a neutral one. Wrapping a two-line calculation in Strategy + Factory + AbstractFactory when nothing in the prompt hinted at variability tells the interviewer you can’t right-size. The correct move is: “I’ll keep pricing a plain method for now; if rules multiply I’d extract a PricingStrategy — here’s the seam.” You get the extensibility credit without the YAGNI penalty.

Phase 3 — Write clean code that actually compiles

This is where most candidates lose points silently. The rules:

  1. Keep it compiling at all times. Stub a method with a return, don’t leave a half-typed expression. A green compile at minute 70 beats elegant code that doesn’t build.
  2. Program to interfaces at the seams you identified — but use concrete classes everywhere else. Don’t interface-ize value objects.
  3. Make illegal states unrepresentable where cheap: enums for status, private fields with controlled transitions, no public setters that break invariants.
  4. Small methods, intention-revealing names. findNearestAvailableDriver, not process2.
  5. Handle the obvious error — request with no available driver, transition from a terminal state — at least with a thrown domain exception. You don’t need every edge, you need to show you see them.

Here’s the kind of clean, interface-driven core the interviewer wants to see — Strategy at the pricing seam, an orchestrator that depends only on abstractions, plus a tiny driver so the “prove it runs” claim is demonstrated by the very code shown. All three tabs round fares to 2 decimals, generate the ride id, and produce identical output for the same inputs.

// --- seams (interfaces) ---
interface PricingStrategy {
price(base: number, demand: number): number;
}
interface MatchingStrategy {
match(drivers: Driver[], rider: Rider): Driver | null;
}

// --- a concrete strategy ---
class SurgePricing implements PricingStrategy {
price(base: number, demand: number): number {
  const multiplier = demand > 1 ? 1 + (demand - 1) * 0.5 : 1;
  return Math.round(base * multiplier * 100) / 100; // 2-dp
}
}

// numeric (non-const) enum: RideStatus[r.status] reverse-mapping works.
// A string/const enum would be safer but would break reverse lookups.
enum RideStatus { Requested, Matched, Ongoing, Completed, Cancelled }

interface Rider { id: string }
interface Driver { id: string; available: boolean }

class Ride {
status = RideStatus.Requested;
constructor(readonly id: string, readonly rider: Rider) {}

private static allowed: Record<RideStatus, RideStatus[]> = {
  [RideStatus.Requested]: [RideStatus.Matched, RideStatus.Cancelled],
  [RideStatus.Matched]:   [RideStatus.Ongoing, RideStatus.Cancelled],
  [RideStatus.Ongoing]:   [RideStatus.Completed],
  [RideStatus.Completed]: [],
  [RideStatus.Cancelled]: [],
};

transition(to: RideStatus): void {
  if (!Ride.allowed[this.status].includes(to))
    throw new Error(`illegal transition ${this.status} -> ${to}`);
  this.status = to;
}
}

// id generation injected so the orchestrator stays runtime-agnostic
type IdGen = () => string;

// --- orchestrator depends only on abstractions ---
class RideService {
constructor(
  private pricing: PricingStrategy,
  private matching: MatchingStrategy,
  private newId: IdGen,
) {}

request(rider: Rider, drivers: Driver[], base: number, demand: number): Ride {
  const ride = new Ride(this.newId(), rider);
  const driver = this.matching.match(drivers, rider);
  if (!driver) throw new Error("no driver available");
  ride.transition(RideStatus.Matched);
  const fare = this.pricing.price(base, demand);
  console.log(`matched ${driver.id}, fare = ${fare}`);
  return ride;
}
}

// --- one-line driver: proves it runs ---
class NearestDriver implements MatchingStrategy {
match(drivers: Driver[], _r: Rider): Driver | null {
  return drivers.find(d => d.available) ?? null;
}
}
let n = 0;
const svc = new RideService(new SurgePricing(), new NearestDriver(), () => `r-${++n}`);
svc.request({ id: "u1" }, [{ id: "d1", available: true }], 99.99, 2);
// -> matched d1, fare = 149.98

Notice what this code does not do: no payment gateway, no persistence, no geo math. Those are stubs by design — and you said so in Phase 1. Scoped narrowness is a feature.

Two implementation notes worth saying out loud, because an interviewer who knows the language will check for them:

  • Id generation is injected (IdGen / new_id / std::function) rather than calling a runtime API inside the orchestrator. TS crypto.randomUUID() would couple the service to a modern runtime (Node ≥19 or a secure browser context with WebCrypto); passing an id generator keeps RideService pure, testable, and runtime-agnostic. Python’s uuid.uuid4() is stdlib-portable and used as the default factory.
  • The C++ transition map intentionally omits terminal-state keys. Completed and Cancelled are not in the map; allowed.find(status_) returns end() for them, which correctly rejects any outbound transition. That is the behavior, not a bug — call it out so the interviewer doesn’t mistake the omission for an oversight.
Tip

The fare line proves parity: for base=99.99, demand=2 all three tabs print 149.98. Earlier drafts of this kind of code silently diverge when C++ skips the std::round(x*100)/100 step — for 99.99 you’d get 149.985 in C++ versus 149.98 elsewhere. If you write multi-language at home, run all three and diff the output. “Equivalent” must mean identical observable behavior, not just similar shape.

Phase 4 — Prove it runs

Always write a tiny driver and run it (or trace it) in front of the interviewer. “It works” is a claim; printed output is evidence. The main() / __main__ / driver block above requests a ride, transitions it to Matched, and prints the fare — that is the proof, shown in the same snippet, not asserted separately.

Strong-hire tell: the candidate wants to run the code and is mildly annoyed when it doesn’t compile, fixes it fast, and keeps going. Hire-but-not-strong: shrugs and says “you get the idea.”

Phase 5 — Absorb the extension volley without flinching

The interviewer will throw a curveball: “Now add scheduled rides” or “support pooling / shared rides” or “make pricing depend on weather.” This is the whole point — they’re testing whether your seams hold. The grade is close to binary:

  • Strong-hire: “Weather pricing is just another PricingStrategy — I add WeatherSurgePricing, no change to RideService. Here’s the class.” (touches one new file, points at the seam)
  • Hire: Adds an if (weather) branch inside the existing pricing method. Works, but the seam didn’t pay off.
  • No-hire: Has to restructure the orchestrator — RideService itself changes because pricing/matching were hardwired. The whole Phase 2 extensibility story collapses on first contact.

The move that earns the most credit is to map the curveball to a seam before you touch the keyboard: “That’s a pricing-rule change, so it lives behind PricingStrategyRideService and Ride don’t move.” Then write the ~8 lines and re-run the driver with the new strategy injected. You’ve demonstrated, live, that your abstraction was load-bearing.

Key idea

The extension volley is graded against your own Phase 2 claims. If you said “pricing is the likely change axis” and then can’t add a pricing rule without surgery on the orchestrator, you scored worse than someone who never made the claim. Only put a seam where you can defend it — and then make sure the curveball flows through it cleanly.

When the curveball lands outside your seams (e.g. “now make it multi-region with per-region driver pools” when you modeled a flat DriverRepository), do not pretend it’s free. The senior move is: “That doesn’t fit my current seam cleanly. The smallest honest change is to make DriverRepository region-aware — one method signature changes and RideService passes a region through. That’s a deliberate, localized change, not a rewrite.” Naming the cost accurately beats hand-waving that “it’s just another strategy” when it isn’t.

Phase 6 — Wrap up and defend trade-offs

With ~5 minutes left, stop coding and narrate. This is free signal that most candidates leave on the table. Hit three things:

  1. What’s in, what’s stubbed, and why — “Pricing and matching are real and extensible; payments and persistence are interfaces with in-memory stubs, by scope.”
  2. The trade-offs you made, as trade-offs — “I used an in-memory map for drivers; O(n) matching is fine for thousands, but I’d swap NearestDriver for a spatial index (geohash / quadtree) at city scale. The interface makes that a one-class change.”
  3. What you’d do with more time — concurrency (a mutex around ride state), idempotency on request, retry/timeout on the payment seam.

Weak close: “Yeah, I think that’s it.” (No reflection, no ownership of the gaps.) Strong close: “Given the clock I prioritized the matching/pricing core and a runnable demo over breadth. The two things I’d harden first are thread-safety on Ride.transition and a real spatial index for matching — both are isolated behind the seams I already drew.”

Watch out

Never defend a choice as universally correct (“Strategy is always better”). Defend it relative to the constraints you were given (“Strategy here because you said pricing rules change weekly; if pricing were fixed I’d inline it”). Interviewers probe specifically to see whether you understand a pattern’s cost, not just its benefit. Treating a trade-off as a trade-off is the senior tell.

The hire vs strong-hire summary

DimensionHireStrong-hire
ScopingAsks a few questionsProposes the cut, writes the contract, confirms
DesignReasonable classesNames seams and the alternatives rejected
CodingCompiles eventuallyCompiles continuously, illegal states unrepresentable
Demo”It would work”Runs a driver, shows output
ExtensionAdds an ifAdds a class through an existing seam, re-runs
Defense”That’s it”Lists trade-offs relative to constraints + next steps

The throughline: a senior candidate is legible. Every decision is spoken, scoped, and defensible. The code is necessary but not sufficient — the round is won in the narration.

Assessment

Pass mark: 70% on the concept questions unlocks the next lesson.

1. It is minute 55 of a 90-minute round. Your core matching/pricing flow compiles and runs, but you have not written a driver/`main()` and you notice your `cancel` transition is untested. The interviewer is quietly watching. What is the strongest next move?

2. In Phase 2 you told the interviewer 'pricing rules are the likely change axis, so I'm putting pricing behind a `PricingStrategy` interface.' In Phase 5 the curveball is 'make pricing depend on weather.' Which outcome scores HIGHEST?

3. You write a tri-language solution where TS/Python round fares to 2 decimals but the C++ `price()` returns the raw `base * mult`. For `base=99.99, demand=2` the tabs print different fares. Which statements are correct? (select all)

4. Your C++ `Ride::transition` calls `std::find`, but your 'includes elided' comment lists only `<string> <vector> <memory> <stdexcept> <iostream> <unordered_map>`. Why is this a real (not merely cosmetic) problem in an interview?

Design problem 5

You are given 90 minutes: 'Design an in-memory rate limiter library that services can call to decide whether to allow or reject a request, keyed by client id.' Walk through how you would run the full round — Phases 1 through 6 — for THIS prompt. Be specific about your scoping cuts, the one or two seams/interfaces you'd commit to, the time you'd allocate, how you'd prove it runs, what extension curveball you'd anticipate and how your seam absorbs it, and how you'd defend one concrete trade-off. Do not write the full implementation; produce the plan and the load-bearing interface.