Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

LLD Interview Cheatsheet
🖨️ Print this page! This cheat sheet contains everything you need to review before an LLD interview.

⏱️ The 45-Minute Framework

┌──────────────────────────────────────────────────────────────────┐
│  PHASE 1: REQUIREMENTS (5 min)                                   │
│  ✓ What are the core features? (ask for top 3-4)                │
│  ✓ Who are the users/actors?                                    │
│  ✓ Any scale/performance constraints?                           │
│  ✓ What's explicitly OUT of scope?                              │
├──────────────────────────────────────────────────────────────────┤
│  PHASE 2: CORE DESIGN (20 min)                                   │
│  ✓ Identify nouns → Classes                                     │
│  ✓ Identify verbs → Methods                                     │
│  ✓ Define relationships (is-a, has-a, uses)                     │
│  ✓ Draw class diagram with key attributes/methods               │
├──────────────────────────────────────────────────────────────────┤
│  PHASE 3: IMPLEMENTATION (15 min)                                │
│  ✓ Code the core happy path                                     │
│  ✓ Apply relevant design patterns                               │
│  ✓ Handle important edge cases                                  │
├──────────────────────────────────────────────────────────────────┤
│  PHASE 4: DISCUSSION (5 min)                                     │
│  ✓ Explain your trade-offs                                      │
│  ✓ Discuss how to extend for new requirements                   │
└──────────────────────────────────────────────────────────────────┘
Pro Move: If running low on time, skip some implementation and explain what you would do. Interviewers value thought process over perfect code.

📋 Requirements Gathering Questions

Always ask before designing:
CategoryQuestions to AskWhy It Matters
ScopeWhat are the must-have features? What can we skip?Prevents over-engineering
UsersWho interacts with the system? (Customer, Admin, System)Defines access control needs
ScaleSingle machine or distributed? How many concurrent users?Threading/locking decisions
DataWhat needs to be persisted? Any real-time requirements?Database design
Edge CasesWhat happens on failure? Concurrent access handling?Shows senior thinking

🏗️ OOP Quick Reference

The Four Pillars - One-Liner Each

PillarWhat It MeansCode Signal
EncapsulationHide internals, expose interfaceprivate fields + public methods
AbstractionDefine WHAT, hide HOWabstract classes, interfaces
InheritanceShare behavior from parentextends / : Base
PolymorphismSame interface, different behaviorOverride methods, common interface
# 1. ENCAPSULATION - Bundle data + methods, hide internals
class Account:
    def __init__(self):
        self.__balance = 0  # Private (use __ prefix)
    
    def deposit(self, amount):  # Public interface
        if amount > 0:
            self.__balance += amount
    
    @property
    def balance(self):  # Read-only access
        return self.__balance

# 2. ABSTRACTION - Define what, not how
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        pass  # Subclasses MUST implement

# 3. INHERITANCE - Reuse and extend
class Dog(Animal):  # Dog IS-A Animal
    def __init__(self, name):
        super().__init__(name)
        self.breed = None
    
    def speak(self):  # Override parent
        return "Woof!"

# 4. POLYMORPHISM - Same interface, different behavior
def make_sound(animal: Animal):
    print(animal.speak())  # Works for Dog, Cat, Bird...

make_sound(Dog("Rex"))   # "Woof!"
make_sound(Cat("Luna"))  # "Meow!"

⚡ SOLID Principles

PrincipleOne-LinerCode Smell
S - Single ResponsibilityOne class = one reason to changeClass has 10+ methods doing different things
O - Open/ClosedOpen for extension, closed for modificationAdding feature requires editing existing classes
L - Liskov SubstitutionSubtypes must be substitutableSubclass throws “not implemented” exceptions
I - Interface SegregationMany specific interfaces > one generalClass implements interface methods it doesn’t need
D - Dependency InversionDepend on abstractions, not concretionsClass instantiates dependencies with new

Quick Examples

# ❌ SRP Violation
class User:
    def save_to_db(self): pass      # Persistence
    def send_email(self): pass       # Notification
    def generate_report(self): pass  # Reporting

# ✅ SRP Applied
class User: pass
class UserRepository:
    def save(self, user): pass
class EmailService:
    def send(self, user, msg): pass

# ❌ DIP Violation
class OrderService:
    def __init__(self):
        self.db = MySQLDatabase()  # Concrete dependency

# ✅ DIP Applied
class OrderService:
    def __init__(self, db: Database):  # Abstract dependency
        self.db = db

🧩 Design Patterns Cheat Sheet

When to Use What?

SituationPatternExample
Need only ONE instanceSingletonDatabase, Logger, Config
Create objects without specifying classFactoryVehicles, Notifications, Payments
Complex object with many optional partsBuilderPizza, HTTP Request, Query
Make incompatible interfaces workAdapterLegacy system integration
Add features dynamicallyDecoratorCoffee toppings, Middleware
Simplify complex subsystemFacadeVideo converter, Payment gateway
Swap algorithms at runtimeStrategySorting, Payment, Shipping
Notify many objects of changesObserverStock price, Notifications
Object behavior changes with stateStateOrder status, Elevator, ATM
Undo/redo operationsCommandText editor, Transactions

Pattern Code Templates

import threading

class Database:
    _instance = None
    _lock = threading.Lock()
    
    def __new__(cls):
        if cls._instance is None:
            with cls._lock:  # Thread-safe
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance
    
    def query(self, sql):
        pass

# Usage
db1 = Database()
db2 = Database()
assert db1 is db2  # Same instance

📊 UML Class Diagram Notation

┌──────────────────────────────────────────────┐
│              ClassName                        │
├──────────────────────────────────────────────┤
│ + publicAttr: Type                           │  + = Public
│ - privateAttr: Type                          │  - = Private
│ # protectedAttr: Type                        │  # = Protected
├──────────────────────────────────────────────┤
│ + publicMethod(): ReturnType                 │
│ - privateMethod(): void                      │
└──────────────────────────────────────────────┘

RELATIONSHIPS:
═══════════════════════════════════════════════
A ────────▷ B     INHERITANCE: A extends B
A - - - - ▷ B     IMPLEMENTATION: A implements B
A ◆───────── B     COMPOSITION: A contains B (B dies with A)
A ◇───────── B     AGGREGATION: A has B (B can exist alone)
A - - - - -> B     DEPENDENCY: A uses B
A ──────────> B     ASSOCIATION: A knows B
═══════════════════════════════════════════════
MULTIPLICITY: 1, 0..1, *, 1..*, n..m

🚨 Common Interview Problems

Parking Lot System

Classes: ParkingLot, Floor, ParkingSpot, Vehicle, Ticket, Payment
Patterns: Strategy (payment), Singleton (parking lot)
Key: Different spot sizes for different vehicles

Elevator System

Classes: Elevator, ElevatorController, Floor, Button, Request
Patterns: State (elevator states), Strategy (scheduling)
Key: Multiple elevators, efficient scheduling algorithms

Library Management

Classes: Library, Book, BookCopy, Member, Lending, Reservation
Patterns: Strategy (search), Observer (notifications)
Key: Book vs BookCopy distinction, reservation queue

Chess Game

Classes: Game, Board, Piece (King, Queen, etc.), Move, Player
Patterns: Factory (piece creation), Command (moves for undo)
Key: Each piece type has unique move validation

Interview Questions

Strong Answer:
  • “How many floors and total spots?” — This determines whether ParkingLot has a List[Floor] (multi-floor with composition) or is a flat structure. A 5000-spot garage needs floor-level indexing for O(1) spot lookup; a 20-spot surface lot does not.
  • “What vehicle types — just cars, or also motorcycles, buses, handicapped?” — This drives the ParkingSpot hierarchy. If buses take 5 contiguous spots, you need a SpotAllocation strategy, not just a 1:1 Vehicle-to-Spot mapping. If handicapped spots exist, you need priority rules and potentially a separate SpotType enum.
  • “Do we need entry/exit gates, or just manual tracking?” — Gates introduce a Gate class with sensor integration, a Ticket class with entry timestamps, and a payment flow on exit. Without gates, the design is simpler but loses the billing model entirely.
  • “Multiple payment methods or just flat rate?” — This decides whether you need a PaymentStrategy interface (cash, card, mobile) or a simple calculate_fee(duration) method. It also determines whether you need a Rate class for hourly/daily/monthly pricing tiers.
  • “Concurrent access — can two cars try the same spot simultaneously?” — This is the question that separates junior from senior designs. If yes, you need a locking mechanism on spot assignment (optimistic locking, mutex, or database-level constraint). If no, the design is simpler but unrealistic.
  • Each answer eliminates or adds 1-3 classes from the design. Without asking, you either over-engineer (designing for buses when the lot only handles cars) or under-engineer (ignoring concurrency in a 5000-spot garage).
Red flag answer: “I already know how to design a parking lot, I don’t need to ask questions” or asking only one generic question like “what’s the scope?” without specifics.Follow-ups:
  1. The interviewer answers: “It’s a multi-floor garage with cars and motorcycles only, flat hourly rate, no gates.” How does this specific set of constraints change your class diagram compared to the “full” parking lot design?
  2. You asked about concurrency but the interviewer says “don’t worry about it.” Should you still design for thread safety, or is that over-engineering?
Strong Answer:
  • Classic trap: “The customer places an order, the system validates the payment, sends a confirmation email, and updates inventory.” Nouns: Customer, Order, System, Payment, Email, Inventory. If you make “System” a class, you have created a God class. If you make “Email” a class, you have confused a message with a service. If you make “Payment” a class, you need to decide whether it is the payment data (amount, method, status) or the payment action (processing, refunding).
  • The heuristic works for core domain objects (Customer, Order, Product) but fails for actions, services, and cross-cutting concerns. “Validates” should not become a ValidatesClass — it should be a method on a ValidationService. “Sends” should not become a class — it is a method on NotificationService.
  • The better approach: identify nouns for entities (things with identity and lifecycle), then identify responsibilities (not just verbs) for services. Ask: “What data does this own?” for entities, and “What behavior does this orchestrate?” for services. This gives you Order (entity: holds items, total, status), PaymentService (service: processes charges, handles refunds), and NotificationService (service: sends emails, SMS).
  • I also look for the “information expert” pattern: assign behavior to the class that has the data needed to perform it. order.calculateTotal() belongs on Order because Order has the items and prices. paymentService.charge(order) belongs on PaymentService because it has the gateway credentials.
Red flag answer: “Just take every noun and make it a class” (mechanical, produces classes like System, Email, Database as domain objects) or “I don’t use any heuristic, I just feel it out” (no structured approach).Follow-ups:
  1. In Domain-Driven Design, there is a distinction between Entities, Value Objects, and Services. How does this map to the noun/verb heuristic, and where does each fit in a class diagram?
  2. The requirement says “the system generates a PDF invoice.” Is Invoice a class, a method, or both? Walk through your reasoning.
Strong Answer:
  • In a real system, the strategy selection is driven by user input (the customer picks their payment method at checkout), configuration (the system supports certain methods per region), or business rules (orders over $500 require wire transfer). The decision lives in a Factory or a configuration service, not in the client code.
  • The pattern in production: the UI sends a payment_method: "paypal" field. A PaymentStrategyFactory maps that string to a concrete strategy: strategies = {"paypal": PayPalStrategy, "credit_card": CreditCardStrategy, "apple_pay": ApplePayStrategy}. The factory returns the strategy, and the CheckoutService uses it without knowing which one it got.
  • The factory itself might be configured per deployment. In the US, you support Stripe and PayPal. In India, you support Razorpay and UPI. The factory reads a config file or feature flag to determine which strategies are available. This means adding a new payment method requires: (1) writing a new strategy class, (2) adding it to the factory mapping, (3) enabling it in the config. No existing code is modified — that is OCP in action.
  • The anti-pattern to avoid: putting the strategy selection in an if/elif chain inside CheckoutService. That defeats the purpose of the pattern because adding a new method requires modifying CheckoutService.
Red flag answer: “The developer just assigns the strategy in the code” (misses the runtime selection mechanism) or “Use a global config variable” (too tightly coupled).Follow-ups:
  1. What happens if the selected payment strategy fails (Stripe is down)? Do you fall back to another strategy, return an error, or retry? How does this affect the Strategy pattern’s design?
  2. How would you handle a payment method that requires additional data (credit card needs card number, bank transfer needs routing number) while keeping the Strategy interface uniform?
Strong Answer:
  • The case for Singleton: some resources genuinely should have only one instance. A database connection pool should be shared to avoid exhausting connections. A configuration loader should read the file once. A logger should funnel all output through one handler to avoid interleaved writes. Singleton enforces this at the language level.
  • The case against: Singleton is global mutable state in disguise. It makes testing extremely difficult because you cannot inject a mock — the class hardcodes its own instance. It hides dependencies (a class uses Database.instance() deep inside a method, invisible from the constructor signature). It creates tight coupling across the codebase. And in multi-threaded environments, the double-checked locking pattern is easy to get wrong.
  • What I use instead: dependency injection with a single instance. Create ONE instance of Database in the composition root (main.py) and inject it into every service that needs it. The effect is the same (only one instance exists) but the benefits are enormous: dependencies are explicit, testing is trivial (inject a mock), and the lifecycle is controlled by the application, not by the class itself.
  • The only case where I still use Singleton: when the instance must be globally accessible from code I do not control (framework hooks, third-party library callbacks). Even then, I prefer a module-level instance in Python (which is effectively a singleton without the pattern overhead).
Red flag answer: “Singleton is great, I use it everywhere” (ignores testability and coupling problems) or “Singleton is always an anti-pattern” (absolutist, ignores legitimate use cases).Follow-ups:
  1. In Python, a module is imported once and cached. Is a module-level variable effectively a Singleton? What are the differences?
  2. How would you test a class that uses Database.instance() internally? Walk me through the refactoring steps to make it testable.
Strong Answer:
  • The core problem is a race condition: User A reads “spot is available,” User B reads “spot is available,” both try to book, and one gets a double-booking. This is the classic check-then-act problem.
  • Option 1: Pessimistic locking. When User A starts booking, you lock the spot row in the database (SELECT ... FOR UPDATE). User B’s read blocks until A commits or rolls back. Pros: simple, guarantees correctness. Cons: reduced throughput, potential deadlocks if locking order is inconsistent.
  • Option 2: Optimistic locking. Each spot has a version number. User A reads version 1, User B reads version 1. User A writes “booked, version 2” — succeeds. User B writes “booked, version 2” — fails because version is no longer 1. User B retries. Pros: high throughput for read-heavy workloads. Cons: retries under high contention, more complex error handling.
  • Option 3: Atomic compare-and-swap at the application level. The book_spot() method uses a single atomic operation: UPDATE spots SET status='booked', user_id=? WHERE id=? AND status='available'. If the row count is 0, the spot was already taken. No explicit lock needed — the database handles atomicity.
  • In an LLD interview, I would model this with a SpotManager class that has a book(spot_id, user_id) method returning a BookingResult with success/failure and reason. I would mention the concurrency strategy as a design decision and ask the interviewer which approach they prefer given the scale constraints.
Red flag answer: “Use a global lock on the entire parking lot” (destroys throughput — only one booking at a time) or “Just check if the spot is available before booking” (ignores the race condition entirely).Follow-ups:
  1. You chose optimistic locking. Under extremely high contention (100 users trying to book the last available spot simultaneously), what happens? How do you prevent “livelock” where everyone keeps retrying and no one succeeds?
  2. How do you represent the locking mechanism in your class diagram? Is it a separate class, a method on ParkingSpot, or invisible at the design level?
Strong Answer:
  • At 10,000 observers and 100 updates per second, you are making 1 million update() calls per second. If each call takes 1ms (network I/O, database write, email send), the notification loop takes 10 seconds — by which time 1,000 more price changes have queued up. The system falls behind and eventually runs out of memory or crashes.
  • The core problem: the naive Observer pattern is synchronous and unbounded. The notify() method iterates through all observers sequentially in the same thread, and there is no backpressure mechanism.
  • Fix 1: Async notification. Instead of calling update() directly, publish a message to a queue (Redis, Kafka, RabbitMQ). Each observer consumes from the queue at its own pace. This decouples the publisher from the consumer’s speed.
  • Fix 2: Batching and throttling. Instead of notifying on every price change, batch updates and notify once per second with the latest price. Most observers do not need 100 updates per second — a 1-second resolution is sufficient for a dashboard.
  • Fix 3: Filtering. Not all observers care about every stock. Add a subscription filter: attach(observer, stock_symbol). The subject only notifies observers registered for the specific stock that changed, reducing the fan-out from 10,000 to perhaps 50 per update.
  • In production at places like Bloomberg or Robinhood, the pattern evolves into a pub/sub system with topic-based routing, consumer groups, and exactly-once delivery guarantees. The Observer pattern is the seed; the production system is the tree.
Red flag answer: “Just use more threads” (does not address the fundamental backpressure problem) or “10,000 observers is unrealistic” (it is not — a stock exchange notification system easily has that many subscribers).Follow-ups:
  1. If an observer’s update() method throws an exception, should it stop the notification loop for all other observers? How do you handle this in your design?
  2. How would you modify the Observer pattern to support “unsubscribe during notification” — an observer that removes itself inside its own update() callback? What concurrency issue does this create?
Strong Answer:
  • Adding Rejected is straightforward: create a RejectedState class that implements handle() by transitioning back to DraftState. ReviewState’s handle() now needs to decide between Published and Rejected. This introduces a branching decision that the original linear chain did not have.
  • The new problem: who decides whether the review passes or fails? The current handle(context) method takes no input. You need to add a parameter: handle(context, action) where action is “approve” or “reject.” Or better, split into explicit methods: approve(context) transitions to Published, reject(context) transitions to Rejected. This makes the valid transitions self-documenting.
  • The deeper design issue is state transition validation. With the linear chain, any call to handle() moves forward. With branching, you need to prevent invalid transitions: you cannot “approve” from Draft (must go through Review first), and you cannot “reject” from Published (already published). Each state must validate the requested action and raise an error for invalid transitions.
  • In production, this is where a State Machine library (like transitions in Python or Spring State Machine in Java) earns its keep. It provides a declarative transition table: transitions = [(Draft, 'submit', Review), (Review, 'approve', Published), (Review, 'reject', Rejected), (Rejected, 'revise', Draft)]. This is easier to audit, test, and extend than scattered if-statements in state classes.
Red flag answer: “Just add an if-statement in ReviewState to check a flag” (breaks the State pattern by reintroducing conditional logic inside states) or “Allow any state to transition to any other state” (no invariant enforcement).Follow-ups:
  1. The product manager says “Published documents should be retractable — they can go back to Draft for corrections.” Now you have a cycle in your state machine. What new risks does a cyclic state machine introduce compared to an acyclic one?
  2. How would you log or audit every state transition for compliance purposes? Where does that cross-cutting concern live in the State pattern — in the states, in the context, or in a separate observer?
Strong Answer:
  • This is a common trap. The interviewer is testing whether I can prioritize simplicity over pattern-matching. I would say: “You are right — let me step back. Let me start with the simplest design that meets the core requirements, and we can add patterns only where the design demands them.”
  • I would strip back to plain classes with direct dependencies. No Strategy pattern unless there are actually multiple interchangeable algorithms. No Observer unless there are actually multiple listeners that need notification. No Factory unless object creation logic is actually complex enough to warrant it.
  • The mistake candidates make: treating patterns as mandatory ingredients rather than tools that solve specific problems. The Singleton pattern exists to solve “I need exactly one instance.” If you do not have that problem, do not use Singleton. The Strategy pattern exists to solve “I need to swap algorithms at runtime.” If there is only one algorithm today, a plain method is simpler.
  • The recovery phrase: “I was anticipating future extensibility, but you are right that the current requirements do not need that complexity. Let me simplify.” Then sketch the simplest solution that works. If the interviewer later asks “how would you add a new payment method?” — THAT is when you say “I would extract a PaymentStrategy interface at that point.”
  • This demonstrates the most senior-level skill of all: knowing when NOT to apply what you know. Restraint is harder than knowledge.
Red flag answer: Doubling down and insisting that the patterns are necessary (arguing with the interviewer) or panicking and removing all structure from the design (overcorrecting to no design at all).Follow-ups:
  1. How do you decide, during a live interview, whether a pattern earns its complexity? What is your personal heuristic for “simple enough to not need a pattern”?
  2. Name a design pattern that you think is almost always worth its complexity, and one that you think is almost never worth it in practice.

❓ Interview Discussion Points

Be ready to answer:
  • “Why did you use inheritance here instead of composition?”
  • “How would you add a new payment method?”
  • “What happens if two users try to book the same spot?”
  • “How would you handle this at 10x scale?”
  • “Where would you add logging/monitoring?”
  • “How would you test this design?”
Power phrases that signal senior-level thinking:
  • “I chose X over Y because…” — Always justify. Interviewers want reasoning, not just decisions.
  • “The trade-off here is…” — Acknowledging trade-offs shows you understand that every design choice has a cost.
  • “This follows the Open/Closed principle because…” — Naming the principle demonstrates conscious design thinking.
  • “Adding new feature Z would just require creating a new class implementing Y” — This proves your design is extensible.
  • “For concurrency, we could use a lock here, though a read-write lock would give us better throughput for read-heavy workloads” — Showing you can reason about performance trade-offs at the design level.
  • “I am separating this into its own service because it has a different rate of change than the core logic” — This is SRP at its most articulate.
  • “Let me start with the happy path and then we can discuss edge cases” — Shows you prioritize and manage scope.

✅ Pre-Interview Checklist

  • Review the 4 pillars of OOP
  • Memorize SOLID principles
  • Know 5-6 key design patterns cold
  • Practice 2-3 problems end-to-end
  • Have your IDE or whiteboard ready
  • Review this cheat sheet!
  • Clarify requirements FIRST (spend 5 min)
  • Think out loud - explain your reasoning
  • Start simple, then add complexity
  • Draw class diagram before coding
  • Apply patterns where appropriate
  • Mention edge cases even if you don’t code them
  • Don’t jump into coding immediately
  • Don’t over-engineer with unnecessary patterns
  • Don’t forget about thread safety for shared resources
  • Don’t ignore error handling completely
  • Don’t make assumptions - ask questions
  • Don’t try to implement everything perfectly

Remember: LLD interviews test your thought process as much as your design. A simple, well-reasoned design beats a complex, unexplained one every time. The candidates who get strong-hire signals are not the ones who write the most code — they are the ones who can explain why they drew each class boundary, why they chose composition over inheritance, and what would change if the requirements evolved. Think of the interview as a design conversation, not a coding test.

Interview Deep-Dive

Strong Answer:
  • I open by restating the problem in my own words to confirm understanding. Then I spend the full five minutes asking clarifying questions, organized by category.
  • Scope: “What are the three or four core features you want me to focus on? What is explicitly out of scope?” This prevents me from over-engineering. If the interviewer says “skip notifications,” I do not waste time designing a NotificationService.
  • Actors: “Who interacts with the system? End users, admins, internal services?” This drives my access control and class hierarchy decisions.
  • Scale: “Is this a single instance or distributed? How many concurrent users should I assume?” This determines whether I need to think about thread safety, locking, or eventual consistency.
  • Constraints: “Are there any specific technology requirements, or should I design language-agnostically?”
  • I write the answers on the whiteboard as a bulleted list so both the interviewer and I can reference them throughout the session. This demonstrates structured thinking before a single class is drawn.
  • The goal is to leave the requirements phase with a shared, explicit scope. When I start drawing classes, every decision traces back to something the interviewer confirmed.
Follow-up: What if the interviewer refuses to answer your clarifying questions and says ‘Use your best judgment’?That is a test of whether I can make reasonable assumptions and communicate them. I would say: “I will assume single-machine scope, three core features, and basic error handling. If any of these assumptions change, the design would adapt in these specific ways.” Then I state my assumptions explicitly on the whiteboard. This shows the interviewer I can operate with ambiguity while remaining transparent about what I am building toward.
Strong Answer:
  • I stop coding and switch to explaining. The worst thing I can do is keep typing and run out the clock silently. Instead, I say: “Let me describe the remaining implementation and the design decisions behind it.”
  • I outline the methods I would implement, the edge cases I would handle, and the patterns I would apply. For example: “The checkout method would use the Strategy pattern for payment, validate the seat lock has not expired, and wrap the database write in a transaction with retry logic.”
  • I also proactively discuss extensibility: “If we needed to add group booking, I would create a GroupReservation class that holds multiple individual reservations and applies a bulk discount strategy.”
  • Interviewers evaluate thought process as much as code. A candidate who explains three remaining methods clearly and discusses trade-offs scores higher than one who writes two more methods in silence but cannot explain why they made any choice.
  • The key phrases: “Here is what I would do next,” “The trade-off I would consider is,” and “This design handles the extension of X by adding a new class implementing Y.”
Follow-up: What is the most common mistake candidates make with time management?Spending too long on requirements or the class diagram and not leaving enough time to write any code. The interviewer needs to see that you can translate a design into working code, not just draw boxes and arrows. My heuristic: stop drawing and start coding by the 15-minute mark. An imperfect class diagram that leads to working code is better than a beautiful diagram with no implementation.
Strong Answer:
  • I approach testing in three layers, matching the structure of the design.
  • Unit tests for individual classes: each class with business logic gets its own test file. I test the PricingService with mock Orders, the PaymentProcessor with a MockPaymentGateway (thanks to DIP, injecting mocks is trivial), and the InventoryService with in-memory data.
  • Integration tests for class interactions: I test that the CheckoutService correctly coordinates PricingService, PaymentProcessor, and InventoryService. These tests use real implementations but with test data. The key scenarios: happy path (order succeeds), payment failure (order rolls back), insufficient inventory (order rejected), and concurrent booking (two users try the same resource).
  • Edge case tests: null inputs, empty collections, boundary values (exactly at capacity, exactly at zero balance), and state transitions that should be invalid (cancelling an already-delivered order).
  • Because the design follows SOLID, each class has a narrow interface and takes its dependencies through the constructor. This means I can test each class in isolation without standing up the entire system. A God class with hardcoded dependencies would require mocking five things just to test one method — that is the testability payoff of SRP and DIP.
  • I would mention but not implement: concurrency tests using threading and assertions about atomicity, and performance tests for hot paths like seat lookup or spot assignment.
Follow-up: How do you decide what NOT to test in a 45-minute interview context?I skip testing getters, setters, constructors, and any boilerplate that the language handles. I focus tests on methods with conditional logic, state transitions, and edge cases. If I have time, I write one test that exercises the full happy path end-to-end. The interviewer is not evaluating test coverage — they are evaluating whether I think about testability as a design concern.