“Design a parking lot. You have forty-five minutes.”
An object-oriented design (OOD) interview rewards procedure over inspiration: six steps, run minute by minute, from the opening question to the extension you volunteer at minute 40.
By the end you will be able to run the whole forty-five minutes without improvising the order:
- which questions to ask, and which assumptions to declare instead of asking;
- how to decide what becomes a class;
- where the one or two interfaces go;
- what to type first;
- what to cut when you are behind.
Everything is defined here. No other chapter is required to follow it.
The one sentence the chapter turns on
A design is a bet about what will change. The framework below is a procedure for finding that bet before you write a class, so that the interviewer’s late requirement lands on an interface instead of on a switch statement.
Four terms in that sentence do all the work. Fix them now, because every later section leans on them.
- Class — a template describing one kind of object: its data, and the operations allowed on that data.
- Object — one filled-in copy of a class.
Ticketis the class; the ticket sitting on your dashboard is an object. - Interface — a promise. A named set of operations with no code behind them, which several different classes can each fulfil in their own way, so the caller never learns which one it got.
- Switch statement — a chain of
if/elifbranches that picks behaviour by inspecting a type or a flag. Every new case is an edit to the same method, which is exactly the cost an interface is bought to avoid.
What goes in and what comes out
In: one underspecified sentence of prompt, plus whatever the interviewer tells you when you ask.
Out: three artifacts.
- A class diagram — one box per class listing its data and its operations, with lines showing which class knows about which.
- 60-150 lines of code that runs.
- A priced answer to a requirement change: what is new, what is untouched, what it cost.
Where this chapter sits
This chapter is the method. Two neighbours fill in around it.
03 is the same vocabulary in depth, derived from running code:
- encapsulation — an object owns its data and nobody reaches in from outside;
- the Liskov substitution principle — anywhere a base class works, a subclass must work too without the caller noticing;
- coupling — how much one class must know about another’s internals;
- cohesion — how well the things inside one class belong together.
Every term this chapter uses is glossed here as it appears, so you can read the two in either order. 04 and the worked problems after it apply the method without re-deriving it.
The six steps and the clock
The whole round fits in one picture and one table: what happens in each block, how many minutes it gets, and what the interviewer is scoring while it happens.
The flowchart below runs top to bottom in clock order. Each box holds four things: the step number, its minute budget, its position on the forty-five-minute clock, and the one thing that step produces.
flowchart TD
S0["0 · Clarify and scope<br/>5 min · 0-5<br/>actors · invariant · what varies"] --> S1
S1["1 · Actors and use cases<br/>5 min · 5-10<br/>verbs before nouns"] --> S2
S2["2 · Objects and relationships<br/>10 min · 10-20<br/>noun triage · cardinalities"] --> S3
S3["3 · The 2-3 decisions<br/>5 min · 20-25<br/>axis of change -> pattern -> cost"] --> S4
S4["4 · Code the core<br/>15 min · 25-40<br/>interface + the one real method"] --> S5
S5["5 · Extend<br/>5 min · 40-45<br/>now add X · which files stay untouched"]
Here is what each box means, with the three pieces of jargon it uses defined on the spot.
- Step 0 — clarify and scope. Find the actors, the invariant, and what varies. An actor is anyone or anything that starts an operation, including a clock or a nightly maintenance job. An invariant is the one fact about your objects that must never be false — “one vehicle per spot” is the parking-lot example used throughout this chapter.
- Step 1 — actors and use cases. Write the verbs down before the nouns.
- Step 2 — objects and relationships. Run the noun triage, then put a cardinality on every line in the diagram. A cardinality is the count written at each end of a line, saying how many of one thing relate to how many of another — one lot has one-or-more levels.
- Step 3 — the two or three decisions. From minute 20 to minute 25, convert each axis of change into a named pattern, and say what that pattern costs in the same breath.
- Step 4 — code the core. Write the interface and the one real method behind it.
- Step 5 — extend. Answer the question the whole round was built around: when the new requirement lands, which files stay untouched?
The table below is the same six steps with the clock made explicit. The column to read hardest is the last one — it is what the interviewer is writing down while you talk.
| Step | Min | Clock | Sub-budget | What is scored |
|---|---|---|---|---|
| 0 Clarify | 5 | 0-5 | 2 min questions, 1 min assumptions, 2 min scope-out | Whether an answer would change a class boundary |
| 1 Use cases | 5 | 5-10 | 2 min actors, 3 min the operation table | Whether you found the verbs before the nouns |
| 2 Objects | 10 | 10-20 | 5 min triage, 5 min diagram | The demotions: which nouns did not become classes |
| 3 Decisions | 5 | 20-25 | 2 min each, plus the cost sentence | Pattern tied to a named change, with its price |
| 4 Code | 15 | 25-40 | 3 min enums and value objects, 3 min the interface, 6 min the core class, 3 min asserts | Whether it runs |
| 5 Extend | 5 | 40-45 | 2 min per scenario | Files new vs files edited |
Step 4 is a third of the clock, so steps 0-3 are setup for it. The most common pacing failure in this round is a beautiful twenty-five-minute diagram followed by fifteen minutes of panic typing. The second most common is the inverse: typing at minute three, with no idea yet what varies, producing a class that has to be deleted at minute twenty.
Step 0 — Clarify and scope (5 min)
This step buys the information that places every interface you will write, then closes the scope so the remaining forty minutes are spent on things that are scored. It has three parts on its own clock: two minutes of questions, one minute stating assumptions, two minutes scoping out.
The six questions worth asking
Ask questions whose answers move a boundary. A question whose answer does not change a class, a cardinality, or an invariant is a question you paid a minute for and got nothing back.
These are the six that pay. The right-hand column is the point of the table: it names the specific fork in your design that each answer decides, so you can see why the question is worth a minute of a forty-five-minute round.
| # | Question | The fork it creates |
|---|---|---|
| 1 | Who acts on this, including the non-humans? | The clock, a sensor, and a maintenance job are actors. Each one you miss is an operation you will not model |
| 2 | What is the one thing that must never be wrong? | Your invariant. It tells you which object must own the mutation, and where the lock goes |
| 3 | What varies, and how often? | Quarterly-changing rules get an interface; never-changing rules get an if |
| 4 | How many X per Y, and can it be zero? | 1 vs 0..1 is a null check; 1 vs 0..* is a collection and a new class |
| 5 | Does this thing have states, and who may change them? | States plus illegal transitions means an enum plus a guarded method, or the State pattern |
| 6 | One process, or is persistence and concurrency in scope? | Decides whether you write a lock and a repository. The recommended default keeps concurrency in — two entrances racing for the last spot is one of the three decisions step 3 ranks as real — and scopes persistence out. Dropping concurrency too buys back about ten minutes, and is the trade you make only when you are already behind at minute 25 |
Seven terms appear in that table and are used everywhere after it. Definitions, shortest first:
- Mutation — any change to an object’s stored data. Who owns the mutation means which single class is allowed to make that change.
- Null check (row 4) — the
if x is Nonethat every optional reference forces on every caller. That is why0..1costs more than1: the optional version buys you a branch in every place that reads it. - Thread — one independently running sequence of steps inside your program.
- Lock — the flag that makes one thread wait, so that a group of steps runs without another thread interrupting halfway through.
- Enum (short for enumeration) — a closed, named set of constants. A spot size can then only ever be motorcycle, compact or large; nothing else can be written into that field by mistake.
- State pattern — gives an object one class per lifecycle phase, so the rules about what is legal now live in the phase itself rather than in a chain of
ifs. - Repository — an object that hides where data is stored behind a few methods like
saveandfind.
The one question that pays for the round
Question 3 — what varies, and how often — is the one that pays for the whole round. Say it explicitly:
“Which of these rules do you expect to change after launch? I want to put the interface exactly there and nowhere else.”
That single question converts a guess into a stated requirement, and it makes every pattern you introduce later defensible by citing the interviewer’s own answer.
The five assumptions every object model rests on
A class diagram is not a picture of the world. It is a picture of what you assumed would vary and what you assumed would hold still, and it is only correct relative to those assumptions. Every object model contains the same five, whether you say them out loud or not, and each comes with a default so that you are never blocked waiting for an answer.
The five are the same in every OOD prompt, whatever the domain. The defaults below are written for the parking lot so they stay concrete; substitute your own nouns.
The rows are lettered A through E and referred to by letter for the rest of the chapter. The last column is the sentence to say out loud when the interviewer does not volunteer an answer.
| Assumption | What it means in plain words | The default to state if nobody tells you | |
|---|---|---|---|
| A | What varies | Which rules the business expects to rewrite after launch | Fees change; the rule matching a vehicle to a spot does not |
| B | What is closed | Which lists of kinds are finished and will never gain a member | Three vehicle sizes: motorcycle, compact, large |
| C | The invariant and its owner | The one fact that must never be false, and the single object allowed to break or restore it | One vehicle per spot, and the lot owns the assignment |
| D | Multiplicity | How many of each thing there are, and whether zero or many is legal | One lot · one-or-more levels · one-or-more spots · at most one open ticket per vehicle |
| E | The boundary | What is deliberately out — stored data that outlives the process, payment, authentication (checking who the user is), more than one machine — and, for contrast, the one hard thing you are deliberately keeping in | Out: persistence, payment, auth, distribution. In: concurrency, because two entrances can race and the lock is scored |
Three of those rows deserve more than a table cell.
A is the assumption the whole design is a bet on, so state it in the form of a bet.
“I am betting the fee rule changes and the fit rule does not, so the fee goes behind an interface and the fit is a method.”
Said that way, the interviewer can correct you in one sentence at minute four, instead of watching you defend the wrong interface at minute thirty-five.
B is the assumption that decides enum versus registry. A registry is a lookup table that maps a name to the thing to use, filled in at startup or at runtime rather than written into the code.
The two branches are not close to each other:
- Closed list. Three sizes is an enum. Adding a fourth is one line.
- Open list. If operators can add categories at runtime, it is not an enum at all. The sizes become rows of data, the fit rule becomes a lookup rather than code, and a
SizeCatalogueclass appears to own that data.
Those two designs share almost no classes, which is why B is worth a sentence out loud rather than a silent guess.
C is the assumption that decides both your field types and where the lock goes.
- “One vehicle per spot” makes occupancy a boolean, and gives
ParkingLotexactly one place to guard. - “A spot may hold two motorcycles” makes occupancy a count, turns
is_free()intohas_room_for(size), and changes every caller that ever asked whether a spot was free.
Which assumptions are load-bearing
An assumption is load-bearing when being wrong about it does not cost you a correction — it costs you the class structure. With ninety seconds of question time and five assumptions on the card, you need a one-line test for telling the load-bearing ones apart from the ones you can simply declare.
The test: flip the assumption to its opposite and ask what the repair costs. If the repair is a method body, state it and keep going. If the repair is a different set of classes, stop and ask.
Run the test on all five. Each row takes one assumption, states its opposite, and works out what the repair would actually cost you. Row D appears twice because flipping it in two different directions gives two different answers.
| Assumption | Flip it | What actually changes | Load-bearing? |
|---|---|---|---|
| A What varies | Fees are fixed forever; the matching rule changes per site | The interface is in the wrong place. Every extension question now lands on a method inside ParkingLot instead of on a new class, and the fee interface you did write is dead weight | Yes — the most load-bearing assumption in the round |
| B What is closed | Operators define their own vehicle categories at runtime | The enum becomes a catalogue object, the fit rule becomes data rather than code, and a class appears that did not exist in the closed design | Yes |
| C Invariant and owner | A spot may hold two motorcycles; the attendant may also assign | Occupancy goes from a boolean to a count, is_free() changes shape, and a second writer means the lock moves to whatever both writers share | Yes |
| D Multiplicity | 3 entrances become 300 | The same classes and a longer list. Nothing moves | No. State it and move on |
| D again, flipped the other way | At most one open ticket per vehicle becomes many | Vehicle gains a collection, and “which ticket does this exit close” becomes a real decision needing an owner | Only across the 0/1/many boundaries — which is why D gets two rows, not because there is a sixth assumption |
| E The boundary | Persistence is in scope after all | A repository interface appears, and the core class must stop constructing its own storage | Yes, but it is yours to declare, not to ask |
Three of the five decide the class structure rather than a method body: A what varies, B which lists are closed, and C the invariant together with its owner.
The other two are cheaper. D moves the code only when it crosses the zero-or-one-or-many boundaries. E is a decision you announce rather than a fact you discover.
That ranking is what makes the next subsection executable when you have two minutes of question time and six things you would like to know.
Here is the failure the ranking prevents. You spend your whole question budget on “how many levels are there?” — the least load-bearing question available — and then discover at minute thirty-five that pricing was the thing the interviewer intended to change. That is a fork you could have found by asking A in ten seconds.
Scope out loud, then move
You will not get answers to all six questions and you should not try; the skill is knowing what to ask in priority order, what to declare without asking, and the exact sentence that buys you the right to proceed.
Ask in this order and stop when the clock says stop. With one question, ask A. With two, add C. With three, add B. Declare D and E yourself — they are yours to set, and no interviewer faults a stated default.
The table is the same instruction as a lookup. Find the row for however much question time you actually have left, ask what is in the middle column, and state the rest.
| If you have | Ask | Declare |
|---|---|---|
| 1 question | A: which rules do you expect to change after launch | B, C, D, E |
| 2 questions | A, then C: what must never be wrong, and who is allowed to change it | B, D, E |
| 3 questions | A, C, then B: is that list of kinds finished, or can operators add one | D, E |
Then write the card below in the corner of the editor and leave it there for the rest of the round.
It is five lines, one per assumption. Each line carries the letter, a short name, the value you settled on, and whether you ASKED for it or STATED it yourself. Every later decision points back at a row, and when the interviewer corrects one you change that row visibly instead of quietly re-deriving.
A varies fee rules change; the fit rule does not ASKED <- LOAD-BEARING
B closed 3 sizes: motorcycle, compact, large ASKED <- LOAD-BEARING
C invariant one vehicle per spot; ParkingLot owns it ASKED <- LOAD-BEARING
D counts 1 lot · 1..* levels · 1..* spots · 0..1 open ticket STATED
E boundary in memory · 1 process · concurrency in scope · no payment, no auth STATED
Two bits of shorthand on that card. The 1..* and 0..1 on row D are cardinality notation, read as “one or more” and “at most one”. Row E’s no auth is short for no authentication, meaning you are not modelling who the user is.
Then fix the rest yourself, in one breath, and name the ones you would revisit:
“I am building this in memory, single process, with concurrency in scope because two entrances can race. No persistence, no payment gateway, no auth. Vehicles are cars, motorcycles and trucks; fees are hourly. If persistence matters, the change is a repository interface behind the lot, and I will point at where it goes rather than write it. Tell me if any of that is wrong.”
“I will point at where it goes rather than write it” is the sentence that buys you the right to stub things. Without it, every stub reads as an omission.
When a correction lands, say which row moved and which classes move with it.
Suppose the interviewer says operators define their own vehicle categories. That is row B being flipped. The honest response is not to nod:
“Then
Sizestops being an enum, the fit rule reads a catalogue instead of amatch, and I need thirty seconds to redraw.”
A load-bearing assumption corrected out loud is a recovery. The same correction absorbed silently is a design that no longer matches its own diagram.
Step 1 — Actors and use cases (5 min)
With scope fixed, the temptation is to start naming classes. Resist it: this step lists the operations before the objects. Two minutes on actors, three on the table below.
Write the verbs down before you write any nouns. Every operation you forget in step 1 is a class you will not discover in step 2, and adding it at minute 35 is the failure mode that looks like a design flaw even when it is a memory flaw.
The whole step fits in one table of four columns, filled in here for the parking lot. Two of the columns are doing design work, not documentation:
- Precondition — what must already be true for the operation to be allowed to run. This column is where your invariants come from.
- Changes what — which data the operation writes. This column is where the ownership questions come from, because whoever changes a thing is a candidate for owning it.
| Actor | Operation | Precondition | Changes what |
|---|---|---|---|
| Driver | park a vehicle | a free spot fits this vehicle | spot occupancy, a new ticket |
| Driver | pay and exit | ticket exists and is unpaid | ticket state, spot occupancy |
| Attendant | close a spot for maintenance | spot is free | spot state |
| Display board | show free counts per level | none | nothing (read-only) |
| Clock | expire an unclaimed reservation | reservation is past its hold window | reservation state, spot state |
Two rows in that table are the ones candidates miss, and both are worth pointing at deliberately.
The first is the non-human actor. A clock or a scheduled job starts operations just as a driver does, and if nothing in your model can be triggered by the passage of time then you have no expiry, no timeout, and no way to answer “what if they never come back for the car”.
The second is the read-only actor — the display board row. It changes nothing, which means it belongs on the outside of your core objects.
That has a concrete consequence: do not put notify_display() calls inside ParkingLot.park(). If the interviewer later asks for live updates, the board is the natural home for the Observer pattern, where interested objects register to be told when something changed, so the thing that changed never has to know who is listening.
What interviewers probe: whether your operations have preconditions. “Park a vehicle” is a feature. “Park a vehicle, given a free spot that fits it, otherwise reject” is a design, because the second half is the branch that needs an owner.
Step 2 — Objects and relationships (10 min)
This step turns the verbs into a diagram: five minutes deciding which nouns deserve to be classes, five minutes drawing the lines between them and labelling what each line claims.
The noun triage, and the trap
Underline the nouns, then refuse most of them. The standard advice — “nouns become classes” — produces a design with twenty classes, fifteen of which have one field and no methods, and it is the single most reliable way to look junior.
A noun earns a class only if it passes all three tests below. Ask the middle column about the noun; if the answer is no, the right-hand column tells you what the noun becomes instead.
| Test | Question | If it fails |
|---|---|---|
| Identity | Are two of these different even when their fields are equal? | It is a value object (@dataclass(frozen=True)) or an attribute |
| State | Does it change over its lifetime? | It is a constant, an enum member, or a value object |
| Behaviour | Does it own a decision others depend on? | It is data: a field on whoever owns the decision |
The identity test needs three terms defined before it means anything.
- Value object — a small immutable thing compared by its contents rather than by which copy it is. Two amounts of five dollars are the same amount, and swapping one for the other changes nothing.
@dataclass— a Python class whose fields are declared once and whose constructor and equality check are generated for you.frozen=True— a@dataclassoption that forbids changing the object after construction.@dataclass(frozen=True)together is the cheapest correct way to write a value object.
A class, by contrast, has identity: two tickets with identical fields are still two different tickets, because one of them is yours.
Now run the three tests on the parking-lot prompt. The verdict column is the answer, and the “why” column names which test decided it. 04 is the full version of this same problem.
| Noun | Verdict | Why |
|---|---|---|
| Parking lot | class | Identity, state, and it owns the assignment decision |
| Level | class | Identity and state; it owns “which of my spots is free” |
| Spot | class | Identity and mutable occupancy |
| Ticket | class | Has a lifecycle: issued, paid, closed |
| Vehicle | class | Identity (a plate), and it answers “what size am I” |
| Size | enum | A closed set of three, compared by value, no behaviour of its own |
| Rate, fee | not a class; a strategy | It is a rule, not a thing. See step 3 |
| Colour, plate, floor number | attribute | No identity, no behaviour |
| Money | value object | Two amounts of 5.00 are the same amount. Frozen, with arithmetic |
| Entrance, exit | attribute or enum, at first | Becomes a class only if it holds state, like a gate or a queue |
Two moves in that table are worth saying out loud, because they are where the signal is.
The demotion
A demotion is a noun you refuse to make a class. Size is the one to narrate:
“
Sizeis an enum, not a class hierarchy. If I madeCompactSpot,LargeSpotandMotorcycleSpotsubclasses, then does this vehicle fit this spot becomes a decision spread across three classes, and adding an EV bay means a fourth subclass plus edits everywhere the fit is computed. As an enum plus one fit-rule object, adding EV is one enum member and one line in the rule.”
The promotion
A promotion is the opposite move: the most valuable class in an OOD answer is often a noun that was not in the prompt, because it is a verb that grew state. Reservation, Payment, Assignment, MaintenanceWindow.
When a use case has a precondition, a duration, or a cancellation, the thing in the middle wants to be an object. “Reserve a spot for fifteen minutes” has all three, which is why Reservation is a class and not a timestamp field on Spot.
Naming one unprompted is one of the cheapest strong signals available in this round.
Relationships, and what the arrows claim
Every line you draw is a claim someone could prove false, and there are four kinds of line.
The table gives the notation used by mermaid class diagrams — the same symbols as UML, the Unified Modeling Language, which is the standard drawing notation for object models. The column that matters most is the last one: it is the yes/no question that tells you which of the four arrows you should have drawn.
| Arrow | Mermaid | Means | The test |
|---|---|---|---|
| Composition | *-- | The part cannot exist without the whole and dies with it | If I delete the whole, do I delete the part? |
| Aggregation | o-- | The whole holds parts it does not own | If I delete the whole, does the part survive elsewhere? |
| Association | --> | Holds a reference, no ownership claim | Can I replace the target without telling the source? |
| Inheritance | <|-- | Substitutable is-a | Can a caller hold the base and never notice the subtype? See 03 |
The last row is the Liskov substitution principle in one line: if Truck inherits from Vehicle, then every place that works with a Vehicle must keep working when handed a Truck, with no special case. When it cannot, the relationship was never is-a.
The fix in that case is to switch mechanisms:
- Inheritance — one class reuses and specialises another. Fixed when you write the class.
- Composition — the whole holds the part and delegates to it. Swappable while the program runs.
Composition is the default and inheritance is the one that needs an argument, for exactly that reason.
The diagram below is three of the four arrows on a deliberately small example — a customer, an order, its lines, and the products they point at. Ignore the class contents for a moment and look only at the three connecting lines: each one uses a different arrow, and each arrow is a different claim about what happens when you delete something.
classDiagram
class Customer {
+place(Order) void
}
class Order {
+total() Money
+cancel() void
}
class OrderLine {
+qty int
}
class Product {
+sku str
}
Customer "1" o-- "0..*" Order : aggregation
Order "1" *-- "1..*" OrderLine : composition
OrderLine "0..*" --> "1" Product : association
Each box is a class with its operations listed inside, and + marks what is public. place, total, cancel, qty and sku are the operations and fields those four classes expose, where sku is the stock-keeping unit, the catalogue’s identifier for a product. The quoted numbers on the line ends are the cardinalities.
Now the three arrows, each stated as a claim a reviewer could prove false:
Order *-- OrderLine: an order line has no meaning outside its order, and deleting the order deletes its lines. Composition is a statement about deletion, not about how the field is spelled.Customer o-- Order: orders survive the customer record for accounting. If you would keep the orders after deleting the customer,o--is right and*--is a lie the diagram is telling.OrderLine --> Product: the line points at a catalogue item it does not own. Delete the product and the line still exists, which is exactly why real systems copy the price onto the line at order time.
Cardinalities are not decoration; each one is a branch in the code. 0..1 is a null check on every read. 1..* means the constructor must reject an empty list. 0..* means a collection, and a collection means somebody has to answer “in what order” and “can it contain duplicates”.
One direction rule that saves you a follow-up: a bidirectional link is two invariants, not one. If Spot knows its Vehicle and Vehicle knows its Spot, then every assignment must update both, and every failure path must unwind both. Pick one owner unless the second direction earns its keep.
Step 3 — The two or three decisions that actually matter (5 min)
This step is where you rank: find the two or three decisions that are real, name the pattern each one implies, and price it before the interviewer asks.
Most of a design is bookkeeping. Two or three things are real, and they are the ones that sit on an axis of change.
An axis of change is a dimension where you already expect a new case to arrive. “A new fee rule every quarter” is an axis. “A new kind of vehicle next year” is an axis. “The lot might have more levels” is not — that is the same code with a longer list.
The test to apply before you introduce any abstraction at all:
Name the second implementation. Out loud. If you cannot, do not write the interface.
That test is the cheapest filter available against pattern-dropping — the failure mode below — because an abstraction you cannot name a second implementation for has, by definition, nothing to abstract over. It also justifies the patterns that survive, because the second implementation you named is usually the interviewer’s own extension question.
The table maps axes of change to patterns. Go in from the left: recognise the axis, ask the question in column two to confirm it is real, and the pattern in column three is what you reach for. Never read column three without reading column four — the cost is the half of the sentence that makes you sound like you have maintained one of these.
| Axis of change | Question that reveals it | Pattern it implies | What it costs |
|---|---|---|---|
| A rule that varies by policy | “Does pricing change per site or per season?” | Strategy | Indirection: the flow now spans two files |
| An object that behaves differently per lifecycle phase | “What can you do to a cancelled one?” | State | More classes than the enum-plus-guard version |
| Choosing a subtype from input | “What decides which kind gets created?” | Factory | One more place to update when a type is added |
| Something wants to know when something else changed | “Does the board update live?” | Observer | Ordering and failure semantics of listeners |
| Requests that must be queued, logged, or undone | “Can they cancel a submitted request?” | Command | Every action becomes an object to construct |
| Global access to one instance | usually nothing | Singleton | Almost always wrong. 03 |
Six patterns are named in that table; the State pattern was already defined back in step 0, and one sentence each is enough for the rest:
- Strategy — puts a varying rule behind an interface, so a new rule is a new small class and nothing else changes.
- Factory — the single place that decides which concrete class to build from some input, so callers never name concrete types.
- Observer — lets interested objects register for change notifications. That is why its cost is about ordering (whose listener runs first) and about what happens when one listener throws.
- Command — wraps an action and its arguments into an object, which is what makes a queue, an audit log, or an undo stack possible.
- Singleton — guarantees exactly one globally reachable instance. It is on this list mainly so you can decline it: global state makes tests share hidden data, and it hides who depends on what.
The announcement script
At minute 20, say your ranking out loud. This is the moment you find out whether it matches the interviewer’s:
“There are three decisions I think are real here: how a vehicle is matched to a spot, how the fee is computed, and what happens when two entrances race for the last spot. The first two are both rules I expect to change, so both are strategies; the third is a correctness problem and I want to show the interleaving. Everything else is bookkeeping. Which do you want first?”
An interleaving, in that script, is one specific ordering in which two threads’ steps could land — the thing you have to show before a lock means anything.
Naming what is not interesting is half of that script’s value. It tells the interviewer you can rank, and it protects the fifteen minutes you need for code.
Step 4 — Code the core (15 min)
This is a third of the round and the only block scored on whether something executes. Surviving it comes down to three habits: a fixed order to type in, four standard stubs with the sentence that makes each acceptable, and one skeleton — every worked chapter in this track is a copy of it.
The order to write it in
Type in the order below. The reason for this particular order is that each layer compiles and is testable before the next one exists, so being interrupted never leaves you with nothing that runs.
| Order | What | Minutes | Why here |
|---|---|---|---|
| 1 | Enums and constants | 2 | Fastest way to make the vocabulary concrete and visible |
| 2 | Value objects (frozen=True dataclasses) | 1 | No behaviour, no risk, and they type the signatures below |
| 3 | The interface(s) from step 3, plus one implementation each | 3 | The design lives here; write it before the class that uses it |
| 4 | The one core class and its one real method | 6 | This is the method the whole round is about |
| 5 | A main with asserts | 3 | Proof. Also your demo script |
A main is the entry point that runs when the file is executed, and an assert is a line that states an expected result and crashes if it is false — which is how a forty-five-minute design proves itself without a test framework.
What to stub, and how to say it
To stub something is to declare its interface, give it the simplest possible implementation, and say out loud what the real one would be. Done with the sentence attached, it reads as scoping. Done silently, it reads as an omission.
Four things are worth stubbing in almost every OOD round. The right-hand column is the sentence to say while you type the stub.
| Stub | The one-liner |
|---|---|
| Persistence | “A Repository protocol with an in-memory dict implementation; the SQL one is the same three methods” |
| Payment | “A PaymentProcessor protocol that returns True; the real one is a network call and a retry policy” |
| Notification | “A listener list. If they want fan-out semantics I will make it Observer” |
| Time | “now() is injected, not datetime.now() called inline, so tests can move the clock” |
Five terms from those four rows:
- Protocol — Python’s name for an interface: a declaration of which methods a collaborator must have, with no code behind it.
- Dict — Python’s lookup table from key to value.
- SQL — the query language relational databases speak. It stands in here for “a real database”. The point of the persistence row is that the dict version and the SQL version live behind the same three methods.
- Fan-out — one event delivered to many listeners.
- Injected — the collaborator is handed in by the caller rather than constructed inside the class. That is what lets a test pass in a fake clock.
The last row is worth doing even when nothing asks for it. Injecting the clock costs one parameter, and it is the difference between a testable design and one where you cannot write the expiry test you are about to be asked for.
The skeleton
Below is the shape of a step-4 answer, written with deliberately generic names — Core, Rule, Threshold — so that it is obviously a template rather than one problem’s solution. It runs as written; the asserts at the bottom are the proof.
Read it in the same five layers the ordering table just gave you:
Status— the enum. The closed set of lifecycle phases.Money— the frozen value object, with a constructor that enforces the promise its docstring makes.RuleandThreshold— the interface from step 3, plus exactly one implementation.Core— the one class with the one real method,submit. Its collaborators (rule,now) are handed in, not constructed inside.- The asserts — the
main.
The two things to watch for are the ones most skeletons leave out: the injected clock now is actually read inside submit, and close() exists so that Status.CLOSED is a state something can reach.
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import Callable
class Status(Enum):
OPEN = "open"
CLOSED = "closed"
@dataclass(frozen=True)
class Money:
"""Value object: non-negative integer minor units, so 0.1 + 0.2 is not a bug.
The docstring is a promise, so the constructor keeps it. Without the
check, Money(1.5) and Money(-500) both construct happily and the
"integer minor units" claim is decoration.
"""
cents: int
def __post_init__(self) -> None:
if not isinstance(self.cents, int) or isinstance(self.cents, bool):
raise TypeError("cents must be an int -- minor units, not dollars")
if self.cents < 0:
raise ValueError("cents must be non-negative")
def __add__(self, other: "Money") -> "Money":
return Money(self.cents + other.cents)
class Rule(ABC):
"""The axis of change. One method, because it is a rule and not a thing."""
@abstractmethod
def applies(self, x: int) -> bool: ...
class Threshold(Rule):
def __init__(self, limit: int) -> None:
self.limit = limit
def applies(self, x: int) -> bool:
return x >= self.limit
class Core:
"""The one class with the one real method. Collaborators are injected."""
def __init__(self, rule: Rule, now: Callable[[], int], deadline: int) -> None:
self._rule = rule
self._now = now
self._deadline = deadline
self._status = Status.OPEN
@property
def status(self) -> Status:
return self._status
def close(self) -> None:
"""The transition. Without it CLOSED is a state nothing can reach."""
self._status = Status.CLOSED
def submit(self, x: int) -> Money:
if self._now() > self._deadline: # the injected clock, actually read
self.close()
if self._status is Status.CLOSED:
raise ValueError("closed")
return Money(100) if self._rule.applies(x) else Money(0)
clock = iter([10, 20, 30])
c = Core(rule=Threshold(limit=5), now=lambda: next(clock), deadline=25)
assert c.submit(7) == Money(100) # t=10, open
assert c.submit(1) == Money(0) # t=20, open
try: # t=30, past the deadline
c.submit(7)
raise SystemExit("should not reach here")
except ValueError:
pass
assert c.status is Status.CLOSED # the expiry test the clock buys
assert Money(100) + Money(50) == Money(150)
print("core ok")
Three details in that code that a first reading skates past:
ABCis short for abstract base class: a class that cannot be instantiated, existing only to declare the promise, with@abstractmethodmarking the operations a subclass must supply.Moneystores integer minor units — cents, not dollars — because binary floating-point cannot represent 0.10 exactly, which is why0.1 + 0.2is famously not0.3.- The
_prefix on_ruleand_nowis Python’s convention for “internal, do not touch from outside”. That is encapsulation stated as a naming habit, and nothing more — see failure mode 2 for exactly how little it enforces.
That skeleton is domain-free on purpose: every worked chapter in this track is that shape with the names filled in. In its eighty-odd lines it demonstrates five things worth demonstrating.
- An enum for a closed set.
- A frozen value object whose constructor enforces the promise its docstring makes.
- One abstract rule with one implementation.
- An injected clock that is actually read on the one real method, so the expiry path is testable.
- Asserts that state behaviour, rather than comments that describe it.
The clock and the close() transition are the two pieces most skeletons omit. Omit either one and Status.CLOSED becomes a state nothing in the class can reach.
To see what “filled in” means, here is each layer of the skeleton next to the parking-lot class that plays its part in 04. Nothing about the shape changes; only the names do.
| In the skeleton | In the parking lot | Why it is that layer |
|---|---|---|
Status enum | VehicleSize (motorcycle, compact, large) | Assumption B: a closed set of kinds |
Money frozen value object | the fee, as an integer count of cents | No identity, compared by value, never a float |
Rule abstract class | FeeModel | Assumption A: the thing you bet would change |
Threshold implementation | HourlyFee | The one implementation you actually write |
Core with submit() | ParkingLot with park() | Assumption C: the class that owns the invariant |
The injected now | the clock handed to ParkingLot | The non-human actor from step 1, injected so tests can move it |
Narrate while typing
Silence while coding is the same defect as silence while whiteboarding. (The design interview playbook has its own list of what not to say; this chapter’s is below.)
One sentence per block is enough. The shape is what I am writing, then why:
“I am making
Moneya frozen dataclass with integer cents, because float money is a bug and because value equality is what I want when I assert on it.”
Step 5 — Extend (5 min)
This is the block the design was for. The interviewer has a change prepared; you should get there first, with a three-sentence answer and a file count that makes the answer checkable.
The drill for each scenario is three sentences, always the same shape:
- What is new — “one new class,
EvFeeModel.” - What is untouched, and why — “
ParkingLot,Level,SpotandTicketare untouched, because none of them ever asks what kind of fee this is.” - What it costs — “reading the pricing flow now means opening the registry and the model, and a typo in the registry key is a runtime failure rather than a compile error.”
The registry in that third sentence is the lookup table introduced back in step 0, here mapping a fee name to the implementation to use.
The cost named there is real, and worth understanding rather than reciting. A mistyped registry key is not caught until the line runs, whereas calling a method that does not exist on a concrete class is caught earlier. You traded a compile-time error for a runtime one, and you should say so.
Then give the diff as file counts, because that is the only metric this round has. Each row is one requirement change, priced twice: once against the design this chapter tells you to build, once against the design you get by default.
| Change | Design with the interface | Design with a switch statement |
|---|---|---|
| Add a fee rule | 1 new file, 1 registry line | Edit the fee method, re-test everything that calls it |
| Add a vehicle size | 1 enum member, 1 line in the fit rule | Edit every if size == chain, and you will miss one |
| Add a per-site override | New composite strategy, 0 edits | Nested conditionals, the method stops fitting on a screen |
A composite strategy in the last row is one strategy object that holds others and combines their answers — a site override wrapping the base fee rule — which is how a second dimension of variation is added without multiplying classes.
If your honest answer to a scenario is “that requires editing five files,” say so. A candidate who diagnoses their own design’s weakness scores above one who claims every change is easy, because the first one has clearly maintained something and the second one has not.
All six steps on one problem
Each step above was demonstrated on the parking lot separately. Run in clock order on that one problem, they sound like this — nothing new, just the whole forty-five minutes end to end. 04 is this same walkthrough at full length, with the code.
Minute 0-5, step 0. Ask A: “which of these rules do you expect to change after launch?” The answer is fees. Ask C: “what must never be wrong?” One vehicle per spot. Declare the rest: three vehicle sizes, one lot with one-or-more levels, in memory, single process, concurrency in scope, no payment and no auth. Write the five-row card in the corner of the editor.
Minute 5-10, step 1. Verbs, with preconditions: a driver parks (given a free spot that fits), a driver pays and exits (given an unpaid ticket), an attendant closes a spot (given it is free), a display board reads free counts, and a clock expires reservations. The clock and the board are the two rows candidates forget.
Minute 10-20, step 2. Triage the nouns. ParkingLot, Level, Spot, Ticket and Vehicle are classes. Size is demoted to an enum. Fee is not a thing at all, it is a rule. Colour and plate are attributes. Draw the boxes and put cardinalities on every line: ParkingLot *-- Level because deleting the lot deletes its levels, Level *-- Spot for the same reason.
Minute 20-25, step 3. Announce the ranking: how a vehicle is matched to a spot, how the fee is computed, and what happens when two entrances race for the last spot. The first two are rules you expect to change, so both are strategies. The third is a correctness problem, so you show the interleaving and then the lock. Everything else is bookkeeping — say that too.
Minute 25-40, step 4. Type in layer order: the VehicleSize enum, the fee as integer cents, the FeeModel interface with HourlyFee behind it, then ParkingLot.park() holding the lock across find-and-claim, then asserts. Inject the clock. Narrate one sentence per block.
Minute 40-45, step 5. Volunteer the extension before it is asked: “add EV charging bays.” Say it in the three-sentence shape. New: enum members for the EV kind, a row in the fit rule, and one fee model. Untouched: Level, Spot and Ticket, because none of them ever asks what kind of fee this is. Cost: the pricing flow now spans two files, and a missing registry key fails at exit time rather than at compile time.
That is the round. If you can say those six paragraphs about a problem you have never seen, you can run this framework out loud.
The four failure modes
Running the steps is half the skill; the other half is not producing one of the four designs that fail this round, listed here in the order interviewers see them. Each one is a rule from the SOLID list broken in a way that shows up as a symptom you can catch in yourself while you are drawing.
SOLID is a five-item checklist for class design. One line each is enough here; 03 derives all five from running code.
- S — single responsibility. A class should have one reason to change.
- O — open/closed. Adding behaviour should mean adding code, not editing code that already works.
- L — Liskov substitution. Anywhere the base class works, a subclass must work too, without the caller noticing.
- I — interface segregation. Several small interfaces beat one large one, so nobody is forced to depend on operations they never call.
- D — dependency inversion. The class holding the policy depends on an interface, and the concrete implementation is handed in from outside.
The four failure modes below map onto that list: the first is S broken, the second is S broken the other way, the third is O and I anticipated too eagerly, and the fourth is O invoked with no change to justify it.
1. The God object
One class holds every responsibility: it assigns spots, computes fees, prints tickets, and talks to the payment gateway.
The tells:
- the class name ends in
Manager,System,Handler, orController; - its method list does not fit on a screen;
- every test needs the whole world constructed;
- any bug fix touches it, so every change has a chance of breaking every feature.
The fix is not “split it into three files.” It is to ask, per method, what does this method need to know about?
In the parking lot, that question sorts cleanly. Methods that need the spot grid stay on ParkingLot. Methods that need only a duration and a rate leave together, and take their data with them — that departing group is the fee model.
That question is cohesion made operational — how well the things inside one class belong together — and the split is right when each resulting class has one reason to change.
2. The anemic domain model
The opposite failure, and the more common one among people who write a lot of services: classes are bags of public fields, and all logic lives in a Service that reaches into them. Anemic here means the objects hold data but own no decisions, so the rules that protect that data live somewhere else entirely.
The code below is the anemic version, and it runs. Order holds a status and nothing else; OrderService.cancel holds the rule “a shipped order cannot be cancelled”. Watch the last four lines: the guarded path correctly rejects the cancel, and then a plain assignment does the exact same thing with nothing to stop it.
from dataclasses import dataclass, field
@dataclass
class Order: # anemic: no behaviour, no protection
status: str = "new"
lines: list = field(default_factory=list)
class OrderService:
def cancel(self, order: Order) -> None:
if order.status == "shipped":
raise ValueError("cannot cancel a shipped order")
order.status = "cancelled"
o = Order(status="shipped")
try:
OrderService().cancel(o) # the guarded path rejects it
raise SystemExit("unreachable")
except ValueError:
pass
o.status = "cancelled" # ...and nothing guards this one
assert o.status == "cancelled" # invariant broken, no error raised
print("anemic model: the rule was bypassed by a plain assignment")
The rule “a shipped order cannot be cancelled” lives in the service, so it is enforced only for callers who happen to use the service. Every new call site is a new chance to bypass it, and there is no compiler check that they did not. The last two lines prove it: a plain assignment walks straight past the guard, and the invariant is now false with nothing raised.
The rich version is next. Two changes: the status becomes an enum instead of a string, and cancel moves onto Order itself, next to the field it protects. The @property gives readers order.status with no setter behind it.
from dataclasses import dataclass, field
from enum import Enum
class OrderStatus(Enum):
NEW = "new"
SHIPPED = "shipped"
CANCELLED = "cancelled"
@dataclass
class Order:
_status: OrderStatus = OrderStatus.NEW
lines: list = field(default_factory=list)
@property
def status(self) -> OrderStatus:
return self._status
def cancel(self) -> None:
if self._status is OrderStatus.SHIPPED:
raise ValueError("cannot cancel a shipped order")
self._status = OrderStatus.CANCELLED
o = Order(_status=OrderStatus.SHIPPED)
try:
o.cancel()
raise SystemExit("should not reach here")
except ValueError:
pass
assert o.status is OrderStatus.SHIPPED # one owner; see the probe below for the limit
print("rich model: the transition is guarded where the state lives")
The second version renames the field to _status to mark it internal and exposes a read-only @property called status.
The transition rule and the state it guards now live in the same class, so there is exactly one place the rule is written. That is a different claim from “the field cannot be assigned from outside”, which would be false. Be precise about which one you are making, because the difference is what an interviewer will push on.
Python enforces nothing here. o.status = ... does raise, because a property with no setter has none — but that is the only route the property closes.
The block below proves it. probe runs each attempt and prints BLOCKED if it raised and OPEN if it did not. Six routes to the field are tried; exactly one is blocked. Note that the third route, the constructor, is the one the demo line in the previous block already used, and the sixth puts a string where an enum member belongs.
import dataclasses
def probe(label, attempt) -> None:
try:
attempt()
print(f"OPEN {label}")
except Exception as exc:
print(f"BLOCKED {label} ({type(exc).__name__})")
probe("o.status = ...", lambda: setattr(o, "status", OrderStatus.CANCELLED))
probe("o._status = ...", lambda: setattr(o, "_status", OrderStatus.CANCELLED))
probe("Order(_status=SHIPPED)", lambda: Order(_status=OrderStatus.SHIPPED))
probe("dataclasses.replace(o, ...)", lambda: dataclasses.replace(o, _status=OrderStatus.NEW))
probe("o.__dict__['_status'] = ...", lambda: o.__dict__.update(_status=OrderStatus.NEW))
probe("Order(_status='banana')", lambda: Order(_status="banana"))
# Only the first is blocked, and the last one is not even a member of the enum.
assert Order(_status="banana").status == "banana"
Three reasons the other five stay open:
- The leading underscore on
_statusis a convention, not an access modifier. It tells a reader “internal, do not touch”, and the interpreter does not enforce it. - The
__init__that@dataclassgenerates takes_statusas a keyword argument, so the constructor is a public door straight into the field. @dataclassdoes no type checking at runtime, so any object at all can end up behind the property — including the string"banana".
What the read-only property actually buys is not a locked door. It is a single reviewable place where the rule lives, plus a bypass that visibly looks like a bypass. o._status = ... in a diff is a reviewer’s flag in a way that o.status = ... on the anemic version never was.
That is a real improvement over the anemic model, where the guard lived in a different class from the data and there was no naming signal at all. It is not the same as making the field private.
You can harden it further — a __setattr__ override, __slots__, object.__setattr__ inside a validating constructor — but a forty-five-minute answer should not carry that machinery, and it still would not close the constructor.
Say the limit out loud instead:
“The property is the one place the rule is written, and
_statusis convention, not enforcement. If I needed real enforcement I would validate in__post_init__and make the field private by name-mangling, and I would still not call it airtight.”
A candidate who knows the limit of their own enforcement scores above one who claims the field is private, because the second claim is checkable in one line and it is wrong.
This is also the whole content of “tell, don’t ask” — call a method that does the work rather than reading an object’s fields and deciding on its behalf — stated as a consequence rather than as a slogan.
3. Premature interfaces
IVehicle, IParkingLot, ISpotRepository, each with exactly one implementation, written before anything needed to vary. The leading I is a naming convention for “this is an interface”, and three of them in a design with no second implementation is the tell.
The cost is real and specific, and it is two costs, not one.
- Every read of the code now requires a jump to find the only implementation.
- The interface freezes a signature you invented before you knew the use cases.
An abstraction extracted from two real cases fits both. One invented from zero cases fits neither.
The rule is the step-3 test: name the second implementation, or do not write the interface. Apply it out loud to your own design at least once during the round.
4. Patterns for their own sake
The Visitor nobody needed. The Factory that wraps a constructor. The Builder for a three-field object. The Observer with one listener that is always present.
Two of those need a definition to see the joke. Visitor adds a new operation across a fixed set of classes without editing them, and it earns its weight only when that set really is fixed. Builder assembles an object through a chain of calls, which pays off at ten optional fields and not at three.
The tell: you can describe the pattern but not the requirement change it is for.
The fix is to reverse the direction — state the change first, then the pattern. “Fees change quarterly, so pricing is a strategy” scores. “I would use the Strategy pattern for fees” does not. They are the same design; only one of them shows you know why.
The strongest single move available in this step is the refusal:
“I am not making the spot-assignment order pluggable. There is exactly one policy today — nearest free spot on the lowest level — and nobody has suggested a second. If one appears it is the same shape as the fee strategy and it goes in the same place. An interface I do not need is a signature I will guess wrong.”
When you are behind
Pacing failures are recoverable if you triage on the clock rather than on hope: one checkpoint that matters, a fixed order to cut in, and three things that are never cut.
The flowchart has one diamond and three exits. The diamond is a question you ask yourself at minute 25; the three arrows out of it are the three honest answers. Two of the three lead to the same cut list, and all of them end at the same never-cut list at the bottom.
flowchart TD
T{"Minute 25:<br/>where are you?"} -->|"still on the diagram"| P1["Emergency: state the objects<br/>in one sentence each, start typing"]
T -->|"just started coding"| P2["On track"]
T -->|"no interfaces yet"| P3["Write the one strategy,<br/>hardcode the rest"]
P1 --> CUT["Cut order:<br/>1 secondary classes<br/>2 the second strategy<br/>3 persistence and stubs<br/>4 the full diagram"]
P3 --> CUT
CUT --> KEEP["Never cut:<br/>the core method that runs<br/>one named axis of change<br/>one extension scenario"]
The three branches, spelled out:
- Still on the diagram. This is the emergency branch. State the objects in one sentence each and start typing immediately.
- Just started coding. You are on track. No recovery needed.
- Code exists, no interfaces yet. Write the one strategy, hardcode the rest, and say out loud that the second implementation is the same shape as the first. The
Thresholdclass in step 4 is the whole template: a constructor and one method.
The cut order comes next. Cut from the top of this table down, and stop as soon as you are back on the clock. Each row is cheap to lose for the reason given, provided you say the sentence in the right-hand column instead of silently omitting the thing.
| Cut first | Why it is cheap to lose |
|---|---|
| 1. Secondary classes | Ticket can be a dataclass with three fields and no methods for now. Say so |
| 2. The second strategy implementation | One implementation plus “the EV one is the same shape as Threshold — a constructor and one method — and goes here” gets the same credit |
| 3. Repositories and stubs | One sentence about where they plug in |
| 4. Diagram completeness | Four boxes with correct cardinalities beat twelve boxes with none |
Never cut: the one core method that actually executes, one named axis of change, and one extension scenario answered concretely. Those three are the round.
Phrases that signal seniority
These are the sentences that make an interviewer stop taking notes and start agreeing. Each one is a claim with a mechanism behind it, which is why it lands.
The left column is lines to actually say; the right column is what the interviewer writes down when you say them.
| Say this | Because it shows |
|---|---|
| “Which of these rules do you expect to change after launch?” | You put interfaces where change is, not where the textbook says |
| “That noun is an enum, not a class - it has no behaviour of its own.” | You triage instead of transcribing |
| “There is a class hiding in that verb: the reservation.” | You find objects the prompt did not name |
| “Composition here, because deleting the level deletes its spots.” | Your diagram makes a claim about lifetime, not about fields |
| “I am not adding an interface there - I cannot name a second implementation.” | Restraint, which is scored |
| “That change is one new class and zero edits, and it costs me one more indirection.” | You price your own patterns |
“The invariant is one vehicle per spot, so ParkingLot owns the assignment and the lock.” | You know that ownership and concurrency are the same question |
| “This rule lives on the entity, so the rule is written once instead of once per call site.” | You know why anemic models rot |
| “Let me inject the clock so the expiry test is possible.” | You have written tests for time-dependent code |
| “If they add a third dimension of variation, strategies multiply and I would switch to composition.” | You know where your own choice stops working |
The entity in the eighth row is the object that owns both a piece of state and the rules protecting it — the rich Order above, rather than the anemic one.
Phrases that hurt
The same content delivered the wrong way round. Each row pairs a sentence that reads as inventory rather than as design with the version that scores. In most rows the two sentences describe the same design — only one of them shows you know why.
| Avoid | Why | Say instead |
|---|---|---|
| “I would use Strategy, Factory and Observer.” | Patterns as an inventory | “One strategy, for fees, because they change quarterly” |
| “I will add getters and setters.” | A public field with extra steps | “It is a frozen dataclass; nobody needs to mutate it” |
“Make Vehicle the base class of Car, Truck, Motorcycle.” | Inheritance for a data difference | “Size is an enum field, because the behaviour is identical” |
| “We would add a lock.” | The lock is the easy half | “Two entrances read the same free spot, both assign; the lock goes around read-and-claim” |
| “It is easy to extend.” | Unfalsifiable | “Adding EV is one enum member and one rule class; here is the diff” |
| “Everything is an object.” | Not a design | “Rules are objects; sizes are enums; amounts are values” |
| “I will use a Singleton for the lot.” | Global state in a costume | “One instance, constructed in main and passed in. 03” |
| “I will model the tables first.” | The database is not the domain | “Objects first; persistence is a repository behind an interface” |
Getters and setters in the second row are one-line methods that only read or write a single field; a class made of them exposes its data as surely as a public field would, while looking like it does not.
Cheat sheet
Everything above compressed to the move and the reason behind it, in clock order, for the ten minutes before the interview. If a row does not make sense on its own, the section it came from is above.
| Moment | The move | The test behind it |
|---|---|---|
| Minute 0 | Scope out persistence, auth, distribution, unless asked | Anything not scored is time stolen from code |
| First question | “What changes after launch?” | The answer places every interface you will write |
| Second question | “What must never be wrong?” | That is the invariant, and its owner takes the lock |
| The assumption card | Five rows: varies, closed, invariant, counts, boundary | Three of them decide the classes, not a method body |
| Use cases | Verbs with preconditions, including non-human actors | A missed operation is a missed class |
| Nouns | Identity, state, behaviour - all three or it is not a class | Most nouns are enums, values, or fields |
| The promotion | Look for the verb that grew state | Reservation, Payment, Assignment |
| Arrows | *-- dies with the whole, o-- survives it, --> just references | Composition is a claim about deletion |
| Cardinalities | On both ends, always | 0..1 is a null check; 1..* is a constructor guard |
| Any interface | Name the second implementation first | Cannot name it, do not write it |
| Minute 25 | Stop drawing, start typing | Step 4 is 15 of the 45 minutes |
| Code order | Enums, values, interface, core class, asserts | Every layer runs before the next exists |
| Minute 40 | Volunteer an extension, with the file diff | New files vs edited files is the only metric here |
| Any pattern | State the change first, the pattern second | “Fees change quarterly, so strategy” |
Next: 03 — OOP Fundamentals — the vocabulary this framework assumes, derived through the consequences that make it worth knowing: what breaks without encapsulation, the Liskov violation that fails an assert, SOLID as five violations and their fixes, and the honest answer about Singleton. Then 04 — Parking Lot System runs all six steps end to end.