On this page
Description
The classic object-oriented design problems - a parking lot, a library, Splitwise, an elevator, a movie booking system - are less about any one pattern than about running the round well: clarifying scope, modeling the domain, getting a thin slice working, and absorbing the follow-up requirement the interviewer adds halfway through. This skill is the playbook for that round; the project skills that follow apply it to each classic problem.
An object-oriented design round hands you a prompt - "design a library system", "build Splitwise", "implement a parking lot" - and 60 to 120 minutes to produce working code in an editor. Two ways to fail it are far more common than not knowing a pattern:
Java
// Minute 75 of 90: a thorough design with nothing that runs
interface Notifier {} // "for later"
abstract class AbstractBookFactory {} // one kind of book exists
class LibraryFacadeBuilder {} // builds... a facade
class Library {
// TODO borrow
// TODO giveBack
}
Python
# Minute 75 of 90: a thorough design with nothing that runs
class Notifier(ABC): ... # "for later"
class AbstractBookFactory(ABC): ... # one kind of book exists
class LibraryFacadeBuilder: ... # builds... a facade
class Library:
pass
# TODO borrow
# TODO give_back
The other failure is the mirror image: one 600-line class that parses input, applies the rules, stores everything in parallel lists and prints results - it runs, but the first follow-up ("now members can reserve books") means editing every method. Graders rarely fail you for missing a clever optimization. They fail you for code that doesn't run, or that can't absorb a change.
We'll go from simple to more technical:
- The five steps and a time budget for a 90-minute round.
- The pieces that pay off in every round: packages, in-memory storage, an injected clock, a demo.
- How rounds are graded.
- The classic failure modes, and the tradeoffs to state out loud.
The five steps
The same five steps work for every prompt; the time budget below assumes a 90-minute round.
| Step | Minutes | What you produce |
|---|---|---|
| 1. Clarify | ~10 | the operations in scope, the rules, what happens on bad input, what's explicitly out |
| 2. Model | ~10 | entities (identity, lifecycle), value objects, enums, the facade that owns the rules |
| 3. API | ~5 | the public methods with signatures, agreed out loud |
| 4. Thin slice | ~40 | one operation working end to end - service, storage, demo - then the rest, one at a time |
| 5. Extend and demo | ~20 | the follow-up the interviewer asks for, a run of the demo or tests, the tradeoffs stated |
- Clarify by asking about operations and rules, not UI: Which operations? What limits? What happens when a book is already on loan - return a value or throw? Is it concurrent? Is persistence needed, or is in-memory fine? Write the answers at the top of your file; they're your spec.
- Model from the operations: the nouns that carry state or identity become classes, closed sets become enums, and one service or facade owns the rules. A subclass hierarchy earns its place only when behavior differs.
- Build the core around the hot operations: pick structures that make them cheap (a map from ID to object, a free list, a running count when reads far outnumber writes), and keep them private.
- Extend where the interviewer pushes - they will add a requirement mid-round. A design with the varying part behind an interface absorbs it with a new class.
The pieces in code
Packages make the design visible at a glance; a thin slice makes it real. For a library:
library/
model/ Book, Loan (records: value types)
repository/ LoanRepository (interface)
InMemoryLoanRepository (a HashMap - no database in the round)
service/ LibraryService (the rules)
Main.java (the demo you run)
Java
public class LibraryService {
private final LoanRepository loans;
private final Clock clock; // injected: tests and demos fix "now"
public LibraryService(LoanRepository loans, Clock clock) {
this.loans = loans;
this.clock = clock;
}
public Loan borrow(String isbn, String memberId) {
if (loans.findByIsbn(isbn).isPresent()) throw new IllegalStateException(isbn + " is already on loan");
Loan loan = new Loan(isbn, memberId, LocalDate.now(clock).plusDays(14));
loans.save(loan);
return loan;
}
}
Python
from collections.abc import Callable
from datetime import datetime, timedelta
class LibraryService:
def __init__(self, loans: LoanRepository, now: Callable[[], datetime]):
self._loans = loans
self._now = now # injected: tests and demos fix "now"
def borrow(self, isbn: str, member_id: str) -> Loan:
if self._loans.find_by_isbn(isbn) is not None:
raise RuntimeError(f"{isbn} is already on loan")
loan = Loan(isbn, member_id, self._now().date() + timedelta(days=14))
self._loans.save(loan)
return loan
Java
public static void main(String[] args) {
Clock clock = Clock.fixed(Instant.parse("2026-03-01T10:00:00Z"), ZoneOffset.UTC);
LibraryService library = new LibraryService(new InMemoryLoanRepository(), clock);
System.out.println(library.borrow("978-0134685991", "m-7")); // Loan[..., due=2026-03-15]
}
Python
def main() -> None:
fixed = datetime(2026, 3, 1, 10, 0, tzinfo=timezone.utc)
library = LibraryService(InMemoryLoanRepository(), now=lambda: fixed)
print(library.borrow("978-0134685991", "m-7")) # Loan(..., due=datetime.date(2026, 3, 15))
if __name__ == "__main__":
main()
In Python the same layout is a package - library/model.py (frozen dataclasses), library/repository.py (an abstract base class or a Protocol, plus a dict-backed class), library/service.py, and a library/__main__.py you run with python -m library.
Three choices here pay off repeatedly in rounds:
- Storage behind an interface, implemented with a
Map. Nobody expects a database in 90 minutes, and "it's aHashMaptoday, a JPA repository tomorrow" is a design point you can show rather than claim. - An injected
Clock(orSupplier<Instant>; in Python, anow: Callable[[], datetime]) instead ofLocalDate.now()scattered through the rules. Anything with due dates, expiry, rate windows or schedules becomes testable - you can demo "15 days later" without waiting. - A
Mainthat exercises the happy path and one rejection. It proves the pieces connect, catches integration mistakes early, and gives the interviewer something to run.
How the round is graded
Most object-oriented design rubrics - at companies that run these rounds for backend roles - score roughly the same things, in roughly this order:
- It works: the required operations run, and the demo or tests show it.
- Structure: sensible classes and packages, each with one reason to change; no god class.
- Extensibility: the follow-up requirement fits without rewriting working code.
- Correctness under edge cases: bad input is rejected clearly, invariants hold, failures change nothing.
- Communication: you stated assumptions, tradeoffs and what you'd do with more time.
Clever optimizations and exotic patterns aren't on the list - they only count when a requirement needs them.
Which tool for which job?
| In the round you need... | Reach for |
|---|---|
| Storage | a repository interface with a map-backed implementation |
| Time (due dates, expiry, windows) | an injected Clock / now callable |
| One rule that varies (pricing, allocation, scheduling) | a strategy interface |
| A closed set of statuses | an enum - a state machine if transitions matter |
| Proof it works | a Main demo (happy path plus one rejection) or a few tests |
| The concurrency follow-up | name the check-then-act race; one lock or an atomic operation |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Modeling details no operation reads | minutes spent on nothing graded | model exactly what the operations need |
| Patterns before any behavior works | interfaces with one implementation, nothing runs | thin slice first, patterns where variation appears |
| Nothing runs at the deadline | scores below a smaller design that works | get one operation working end to end early |
| One class doing parsing, rules, storage and output | every follow-up touches everything | a service per area of rules, storage behind an interface |
Silent failures (false, null, swallowed exceptions) | edge-case questions expose them | reject clearly, consistently |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. At minute 60 of 90 you have a clean class diagram and interfaces, but nothing compiles end to end. What should you do next?
Show answer
Stop designing and make one operation work end to end - service, storage and a Main that calls it - then add the others one at a time. A running thin slice scores above a complete-looking design that doesn't run, and it's the base any follow-up will be built on.
2. Why inject a Clock into a service that computes due dates, instead of calling LocalDate.now() inside it?
Show answer
So tests and demos can control time: fix the clock to a known instant, or advance it, and the due dates, late fees and expiry rules become deterministic. With now() inside the rules, "what happens after 15 days?" can't be shown without waiting 15 days.
3. Halfway through, the interviewer says: "Members can now also reserve a book that's on loan." Your borrow method is the only place that knows about availability. What's the good response?
Show answer
Treat it as the test it is: add the new concept (a reservation queue per book), keep the rule changes inside the service that owns availability, and make giveBack hand the book to the first reservation. Say out loud which existing code changes and why - a change that stays inside one class shows the design absorbed it.
Key takeaways
- The round grades working, readable, extensible code - in that order. A thin slice that runs beats a complete design that doesn't.
- Clarify operations and rules first; model from the operations; agree the API before coding.
- Packages plus a
Maindemo make the design visible; in-memory repositories and an injectedClockkeep it testable. - Add patterns only where the requirements vary - and expect the interviewer to add a requirement mid-round.
- State assumptions and tradeoffs out loud: what's out of scope, what production would add, where concurrency would bite.