Engineering Atlas › Low-Level Design

Solving an object-oriented design question

15 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

What the interviewer is looking for

Most rubrics for OOD or machine-coding rounds score four things. Requirements handling: did you ask questions that change the design, and state what is out of scope? Modelling: are the classes cohesive, is ownership clear, do relationships match the domain? Code quality: does the core run, are invariants enforced by the API rather than by hoping callers behave? Extensibility and depth: can you absorb a new requirement in a few classes, and do you know where locks go? The step-by-step approach below maps to these four.

Step 1: Clarify use cases (5 minutes)

Turn the prompt into five to eight use cases, each phrased as actor plus verb plus object: "driver parks a car", "driver pays and exits", "attendant views free spots by floor". Ask questions whose answers change classes: are there spot sizes? Is pricing hourly, flat or tiered, and will it change? One building or many? Is this in-process, or do you need persistence? Concurrent entrances? Write the list down where the interviewer can see it, and name what you are leaving out (payments provider integration, reservations, a UI). The Use Case Diagram lesson covers how to draw this; in an interview a bullet list is enough.

Step 2: Find the entities (5 minutes)

Underline nouns in the use cases and keep the ones that hold state or rules: ParkingLot, Floor, Spot, Ticket, Vehicle, Payment. Drop nouns that are attributes (colour, licence plate) or actors outside the system (Driver, unless it has state you track). Separate entities, which have identity and a lifecycle (a ticket is issued, paid and closed), from value objects, which are compared by value (a Money amount, a TimeRange). Classes and Objects and Enums go deeper on that split.

Step 3: Assign every use case an owner

For each use case, name the class and method that will carry it out: "driver parks a car" belongs to ParkingLot.park(vehicle) -> Ticket; "driver pays" belongs to Ticket.pay(amount, clock) or a PaymentService. This is the step most candidates skip, and it is where designs succeed or fail. Two heuristics help. Give the responsibility to the class that has the information it needs (the GRASP "information expert": the ticket knows its entry time, so it computes duration). And use one entry-point class per subsystem (a controller or facade) so callers do not orchestrate five objects themselves.

Then run three checks. A use case with no owner is a missing requirement. A class that owns no use case and is not used by a class that does is an orphan: delete it or find its job. A class that owns most use cases or has 25 methods is a god class: split it along its responsibilities. The practice task automates exactly these checks.

Step 4: Relationships and multiplicities (5 minutes)

Draw a class diagram with only the arrows that matter. Is it composition (a Floor owns its Spots and they die with it) or association (a Ticket refers to a Spot it does not own)? What are the multiplicities (one lot to many floors; one spot to zero or one active ticket)? The Class Diagram, Association, Aggregation and Composition lessons cover the notation. Interviewers mostly care that ownership is right, because ownership decides who enforces invariants.

Step 5: Public API and invariants

Write the signatures of the entry-point methods, including errors: park(vehicle) -> Ticket fails with LotFull; unpark(ticket_id, clock) -> Receipt fails on an unknown or already closed ticket. Then state the invariants in one line each: a spot holds at most one vehicle; a ticket is closed at most once; free-spot counts equal the number of unoccupied spots. Design the API so callers cannot break them, which usually means no public setters on state that has rules.

Step 6: Lifecycles as state machines

Any entity with stages (a ticket, a booking seat, a vending machine, an elevator car) gets a small state diagram: states, events and which transitions are legal. An enum plus a transition table is often enough; the State pattern earns its keep when behaviour differs a lot per state, as in Design Vending Machine. The State Machine Diagram lesson covers hierarchical states and guards.

Step 7: Code the core (15 to 20 minutes)

Implement the entry-point class and the two or three operations that carry the invariants. Inject a clock and any randomness so behaviour is deterministic. Use maps for lookups by id, and a heap or ordered structure where you need "nearest free spot". Skip getters, logging, persistence and input parsing unless asked. Write one or two quick tests or a main that walks a scenario; it proves the code runs and shows you think about verification.

Step 8: Concurrency, edge cases and change (5 to 10 minutes)

Expect probes: two cars at two entrances take the last spot at the same moment; a payment succeeds but the exit gate fails; pricing changes at midnight; a second building opens. Answer with the smallest change: one lock around the allocation step (or per-floor locks if contention is real), idempotent payment keyed by ticket id, a PricingPolicy interface introduced now because the interviewer just told you pricing varies, and a Lot registry keyed by building. This is where patterns appear, each tied to a stated change.

Worked examples in this course

Each design lesson applies this approach end to end and is worth doing under a timer:

Working with an AI assistant

In practice rounds, an assistant can generate a full parking-lot class hierarchy in seconds, and it will usually include a ParkingLotManager, a Singleton, and getters for everything. Use it as an opponent rather than an author: produce your own use-case-to-owner table first, then ask the assistant for its design and compare ownership decisions. Where they differ, decide which is right and why. That trains the judgement the interview tests.

The same shape elsewhere

"Every requirement has exactly one owner, and nothing exists without a purpose" is the same check used in other fields. A RACI matrix in project management assigns one accountable person per task and flags tasks without one; a traceability matrix in regulated software maps each requirement to the code and tests that satisfy it; on-call rotations assign every alert an owning team. Orphan alerts and orphan classes fail the same way: nobody acts on them.

Down the stack

Layer 1The contract

The interview's contract is the use-case list you wrote in step 1. Your design promises that each use case is reachable through a public method on an owning class, that each stated invariant holds after any sequence of public calls, and that each error case returns a specific error rather than corrupting state. Stated that way, the contract is testable: you can write a test per use case and a property test per invariant, which is exactly what strong candidates sketch at the end.

Layer 2The mechanism

The method underneath is responsibility assignment. CRC cards (Class, Responsibilities, Collaborators), introduced by Beck and Cunningham in 1989, put each class on an index card with what it knows, what it does and who it talks to; if a card overflows, the class does too much. GRASP adds named rules: information expert (the class with the data does the work), creator (the class that contains or aggregates B creates B), controller (one class receives system events), low coupling and high cohesion. Tell, don't ask is the code-level form: call ticket.close(now) rather than reading ticket.status and writing it from outside. The use-case-to-owner table is a mapping from requirements to methods, and the three checks (no unowned use case, no orphan, no god class) are graph properties of that mapping plus the class relationships.

Layer 3The machine

Interviewers probe the runtime when they ask about scale and concurrency, so know what your object graph costs. A parking lot with 10,000 spots modelled as objects is about 10,000 small heap objects; in Go or Java that is a few hundred kilobytes, trivial. A map from ticket id to ticket costs one hash and usually one cache miss (about 100 ns from main memory) per lookup. An uncontended mutex in Go or Java costs about 20 to 50 ns; under contention a waiting goroutine parks and wakes in microseconds, so a single lock around park() handles thousands of entries per second, far beyond any real building. A per-request lock across a network call (charging a card inside the lock) is the real hazard: it holds the lock for 200 ms and serialises every entrance. The machine-level answer is to keep lock scopes to in-memory state changes and do I/O outside them.

Why it's built this way

  1. Requirements arrive as sentences, and code needs owners for each behaviour, so the first job is mapping sentences to methods.
  2. Without an explicit owner per use case, behaviour gets split across classes or duplicated, and invariants break in the gaps.
  3. Classes without a use case cost reading time and maintenance while serving nothing, so they are removed.
  4. Classes that own everything cannot be changed safely or tested in parts, so responsibilities are split by cohesion.
  5. Interviews are 45 minutes, so the process front-loads decisions that are expensive to reverse (ownership, lifecycles) and leaves cheap ones (patterns, naming) for last.

Diagram

flowchart TD
  P["Prompt"] --> U["1. Use cases and scope"]
  U --> E["2. Entities and value objects"]
  E --> O["3. Owner per use case"]
  O --> K{"Checks pass?"}
  K -- "unowned use case" --> E
  K -- "orphan or god class" --> O
  K -- "yes" --> R["4. Relationships"]
  R --> A["5. API and invariants"]
  A --> S["6. Lifecycle state machines"]
  S --> C["7. Code the core"]
  C --> X["8. Concurrency and change"]
  X -- "new requirement" --> O

Worked example

Prompt: "Design a library management system." Forty-five minutes.

Minutes 0 to 5. Use cases: member borrows a copy; member returns a copy; member reserves a title that has no free copy; librarian adds copies; system computes late fines. Out of scope: search UI, payments, multiple branches. Questions asked: max loans per member (5), loan length (14 days), fine (20 cents a day, capped at 1,000), and whether reservations are first come, first served (yes).

Minutes 5 to 12. Entities: Library, Member, Title (an ISBN and metadata), Copy (a physical book with a status), Loan, Reservation. Value objects: Money as integer cents, Date. Owner table: borrow and return go to Library.borrow(member_id, copy_id, today) and Library.return_copy(copy_id, today) as the controller; the fine is computed by Loan.fine(today) (information expert: the loan knows its due date); reservations by Title.reserve(member_id), since the queue is per title. Checks: every use case owned; no orphans; Library owns 3 of 5 use cases, which is the controller's job and acceptable, with 6 methods.

Minutes 12 to 18. Relationships: Library composes Titles, Title composes Copys, Loan associates one Member with one Copy. Invariants: a copy is on at most one open loan; a member has at most 5 open loans; a reserved copy can only be borrowed by the first member in its title's queue. Copy lifecycle: available, on loan, held for reservation, lost.

Minutes 18 to 38. Code Library.borrow, Library.return_copy, Loan.fine and the reservation hand-off on return, with an injected today. A short scenario test borrows, returns 3 days late (60 cents), and checks the next reserver's hold.

Minutes 38 to 45. Probes: two members borrow the last copy at once (a lock around the copy's status change in borrow, or a compare-and-set on the status); fine rules differ for students (a FinePolicy interface, introduced now because the requirement is real); a second branch (a Branch above Library, with copies owned by branches and transfers as a new use case).

Trade-offs

OptionStrengthsWeaknessesChoose when
Rich domain objects (behaviour on entities)Invariants live next to data; tell, don't askBehaviour spread across many classesEntities have real rules and lifecycles
Service classes over plain dataEasy to find all logic; simple dataAnemic model; invariants enforced by conventionLogic spans many entities with few per-entity rules
One controller per subsystemClear entry point; easy to test end to endCan grow into a god classMost interview problems
Patterns from the startReady for variationOver-engineering if variation never comesThe interviewer has stated what varies
Code first, model laterFast to a running programOwnership decisions made by accidentPure machine-coding rounds with a strict spec

Pitfalls in production

Leadership lens

Check yourself

  1. Why assign an owner to each use case before drawing relationships?

    Show answer

    Ownership decides which class enforces which invariant, and relationships follow from who needs whom to do their job; drawing arrows first produces structure without responsibility.

  2. A Ticket class has fields and getters only, and ParkingService computes fees, closes tickets and frees spots. What is wrong and how would you fix it?

    Show answer

    It is an anemic model with a god service, so ticket rules can be broken from anywhere; move fee and closing logic onto Ticket (it has the entry time and status) and keep the service as a thin coordinator.

  3. When is it right to add a Strategy interface during the interview?

    Show answer

    When the interviewer states, or you confirm, that a behaviour (pricing, fines, dispatch) varies or will change; without that, a plain method is the stronger answer.

  4. Two entrances try to take the last spot at once. What is the smallest correct fix?

    Show answer

    Make the check-and-allocate step atomic with one lock (or a compare-and-set on the spot), and keep any I/O such as printing or charging outside the lock.

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →