Design Projects: Classic Systems · How to Approach an Object-Oriented Design Interview

How to Approach an Object-Oriented Design Interview

Running an object-oriented design interview: scoping, modeling, a working slice first, and extensions on demand.

  • 7 min read
  • 13 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The five steps
  3. The pieces in code
  4. How the round is graded
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

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:

  1. The five steps and a time budget for a 90-minute round.
  2. The pieces that pay off in every round: packages, in-memory storage, an injected clock, a demo.
  3. How rounds are graded.
  4. 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.

StepMinutesWhat you produce
1. Clarify~10the operations in scope, the rules, what happens on bad input, what's explicitly out
2. Model~10entities (identity, lifecycle), value objects, enums, the facade that owns the rules
3. API~5the public methods with signatures, agreed out loud
4. Thin slice~40one operation working end to end - service, storage, demo - then the rest, one at a time
5. Extend and demo~20the 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 a HashMap today, a JPA repository tomorrow" is a design point you can show rather than claim.
  • An injected Clock (or Supplier<Instant>; in Python, a now: Callable[[], datetime]) instead of LocalDate.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 Main that 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:

  1. It works: the required operations run, and the demo or tests show it.
  2. Structure: sensible classes and packages, each with one reason to change; no god class.
  3. Extensibility: the follow-up requirement fits without rewriting working code.
  4. Correctness under edge cases: bad input is rejected clearly, invariants hold, failures change nothing.
  5. 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
Storagea 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 statusesan enum - a state machine if transitions matter
Proof it worksa Main demo (happy path plus one rejection) or a few tests
The concurrency follow-upname the check-then-act race; one lock or an atomic operation

Common mistakes

MistakeWhat happensDo this instead
Modeling details no operation readsminutes spent on nothing gradedmodel exactly what the operations need
Patterns before any behavior worksinterfaces with one implementation, nothing runsthin slice first, patterns where variation appears
Nothing runs at the deadlinescores below a smaller design that worksget one operation working end to end early
One class doing parsing, rules, storage and outputevery follow-up touches everythinga service per area of rules, storage behind an interface
Silent failures (false, null, swallowed exceptions)edge-case questions expose themreject 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 Main demo make the design visible; in-memory repositories and an injected Clock keep 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.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

In the round you need...Reach for
Storagerepository interface + map-backed class
Timeinjected Clock / now
A varying rulea strategy interface
Statusesan enum (a state machine if transitions matter)
Proof it worksa Main demo or a few tests
The concurrency follow-upone lock, an atomic operation, a conditional update
Retriesan idempotency key

Snippets worth knowing by heart

Java

public synchronized Loan borrow(String isbn, String memberId, String requestId) {
    Loan previous = byRequestId.get(requestId);
    if (previous != null) return previous;                         // a retry: same result
    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);
    byRequestId.put(requestId, loan);
    return loan;
}

Python

def borrow(self, isbn: str, member_id: str, request_id: str) -> Loan:
    with self._lock:
        if request_id in self._by_request:
            return self._by_request[request_id]                    # a retry: same result
        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)
        self._by_request[request_id] = loan
        return loan

The traps

  • Over-scoping and patterns before behavior.
  • Nothing runs at the deadline.
  • A god class.
  • Silent failures and mixed error signals.

Staff-level points, one line each

  • Check-then-act races oversell the last item: 8 threads, more than 1 confirmed; synchronized/a lock kept it at 1.
  • Python races too once anything runs between check and act - lock both.
  • Keep critical sections short; never hold a lock across a payment call.
  • Idempotency keys make retries return the first result (one loan, not two).
  • One error contract: exceptions for broken rules, Optional/None for absence, result types when failures are routine; failures change nothing.
  • Hot paths pick structures: sorted free sets per size, running counts.
  • Know each problem's usual follow-up and leave the seam (a strategy, a queue, a lock).
  • A few behavior-named tests right after the thin slice.

Say it in an interview

  • "First, which operations are in scope, and what happens on bad input?"
  • "Two requests for the last seat would race here - I'd make reserve atomic."
  • "Retries carry a request id, so a timeout can't create a second loan."
  • "Pricing is the part that will change, so it's behind an interface."

Practice

This skill comes with 13 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice How to Approach an Object-Oriented Design Interview free

Test the Meeting Room Calendar

MediumProTest-writing taskJava

A coworking space in Dublin books its meeting room through RoomCalendar (in the project, read-only). Before it goes live, write the tests for it in RoomCalendarTest.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice How to Approach an Object-Oriented Design Interview free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime