The 50 low level design questions interviewers actually ask, with direct answers, class sketches, real code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
50 questions with answersKey Takeaways
Low level design is the step where a feature becomes concrete code structure: the classes, their fields and methods, the interfaces between them, and how objects collaborate at runtime. It answers questions high level design leaves open, like which class owns which responsibility, how a Parking Lot tracks free spots, or how a payment flow stays open to new methods without editing old code. An LLD interview, sometimes called object-oriented design or the machine coding round, hands you a problem like "design an elevator system" and watches how you clarify requirements, model the entities, apply the SOLID principles, and reach for a design pattern only when it earns its place. The Refactoring Guru design patterns catalog is the canonical reference for the pattern half of that skill. Good answers stay grounded: you name responsibilities before classes, keep coupling low, and don't reach for a pattern where a plain method would do. This page collects the 50 questions that come up most, each with a direct answer plus class sketches and real code. Pair it with our AI interview preparation guides, because the first round is increasingly an AI-driven technical screen.
Watch: 10 Design Patterns Explained in 10 Minutes
Video: 10 Design Patterns Explained in 10 Minutes (Fireship, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your Low Level Design certificate.
The fundamentals every entry-level round checks: what LLD is, the four OOP pillars, the SOLID principles, and the basic class-modeling vocabulary. If any answer here surprises you, that's your study list.
Low level design turns a feature into concrete code structure: the classes, their fields and methods, the interfaces between them, and how objects collaborate at runtime. It's detailed enough that a developer can pick it up and implement it directly, without more design decisions.
High level design works one level up, at the system: which services exist, which database, how components scale and talk over the network. So HLD decides you'll have a payments service; LLD decides the classes inside it, like Payment, PaymentMethod, and how a new method plugs in. LLD is about the object model, not the deployment map.
Key point: Interviewers open with this to check you answer at the right altitude. Say 'classes and their collaboration inside one service', not services and databases.
Encapsulation bundles data with the methods that operate on it and hides internal state behind an interface. Abstraction exposes only what a caller needs and hides the how. Inheritance lets a class reuse and specialize another class's behavior. Polymorphism lets one interface work with many types, so a caller treats different objects uniformly.
In an LLD round these aren't trivia: encapsulation keeps state consistent, abstraction defines clean interfaces, and polymorphism is what makes a design extensible. the key signal is whether you use them to justify class boundaries, not just recite the definitions.
Key point: each pillar connects to a design benefit. 'Polymorphism is how I keep the code open for extension' works better than a textbook definition.
Watch a deeper explanation
Video: Object-Oriented Programming, Simplified (Programming with Mosh, YouTube)
Encapsulation is about hiding internal state and controlling access to it: private fields, public methods, so nothing outside the class can put it in a bad state. Abstraction is about hiding complexity behind a simple interface: the caller knows what a method does, not how.
They're related but not the same. Encapsulation is the mechanism (access control, keeping data and behavior together); abstraction is the goal (exposing a clean, minimal surface). You use encapsulation to achieve abstraction.
Key point: The trap is treating them as synonyms. Saying 'encapsulation is the how, abstraction is the what' is the clean distinction the question needs.
Composition builds a class's behavior by holding other objects (a Car has-an Engine) instead of inheriting from a base class (a Car is-a Vehicle). 'Prefer composition over inheritance' means reach for has-a relationships first, because they're more flexible and less brittle.
Deep inheritance couples a subclass tightly to its parent: a change in the base class ripples down, and you can't easily mix behaviors from multiple sources. Composition lets you swap parts at runtime and combine behaviors freely. Inheritance still fits true is-a relationships, but composition is the safer default.
// inheritance couples Duck to a fixed fly behavior
// composition lets you swap it at runtime
interface FlyBehavior { void fly(); }
class Duck {
private FlyBehavior flyBehavior;
Duck(FlyBehavior fb) { this.flyBehavior = fb; }
void performFly() { flyBehavior.fly(); }
void setFly(FlyBehavior fb) { this.flyBehavior = fb; }
}Key point: Give the reason, not just the slogan: inheritance couples tightly and can't combine behaviors. Naming the fragile-base-class problem shows depth.
SOLID is five design principles for maintainable object-oriented code. Single Responsibility: a class has one reason to change. Open/Closed: open for extension, closed for modification. Liskov Substitution: subtypes must be usable in place of their base type. Interface Segregation: prefer small focused interfaces. Dependency Inversion: depend on abstractions, not concrete classes.
Together they push toward loose coupling and easy extension. In interviews you're expected to name them and, more importantly, apply them: when you split a class, you're citing SRP; when you add a method through a new class instead of an if-else, that's Open/Closed.
| Letter | Principle | One-line rule |
|---|---|---|
| S | Single Responsibility | One class, one reason to change |
| O | Open/Closed | Extend without editing existing code |
| L | Liskov Substitution | Subtypes must substitute cleanly |
| I | Interface Segregation | Small, focused interfaces |
| D | Dependency Inversion | Depend on abstractions |
Key point: Don't just recite the five. the question needs you to point at your own design and say which principle each decision follows.
Watch a deeper explanation
Video: Intro to Object Oriented Programming - Crash Course (freeCodeCamp.org, YouTube)
The Single Responsibility Principle says a class should have one reason to change, meaning one focused responsibility. If a class both formats a report and saves it to disk, two unrelated changes (a new format, a new storage target) both force edits to the same class, and the two concerns get tangled.
The fix is to split them: a ReportFormatter that builds the content, and a ReportSaver that persists it. Now each changes for its own reason, they can be tested separately, and you can reuse the formatter with a different saver. SRP is the principle behind most 'this class is doing too much' feedback.
Key point: Anchor it to 'one reason to change', not 'one method'. A class can have many methods and still hold a single responsibility.
Open/Closed says software should be open for extension but closed for modification: you add new behavior without editing existing, working code. The classic smell it fixes is a growing if-else or switch that you edit every time a new type appears.
You apply it with polymorphism. Instead of a switch on shape type inside an area function, you define a Shape interface with an area() method and add a new class per shape. Adding a Triangle means writing a new class, not touching the old ones, so you can't break what already works.
// closed for modification, open for extension
interface Shape { double area(); }
class Circle implements Shape {
double r;
public double area() { return Math.PI * r * r; }
}
// add Triangle as a new class; existing code is untouched
class Triangle implements Shape {
double base, height;
public double area() { return 0.5 * base * height; }
}Key point: Show the before (a switch you keep editing) and after (a new class per type). the question needs the mechanism, not just the definition.
The Liskov Substitution Principle says an object of a subtype should be usable anywhere its base type is expected, without breaking correctness. If code works with a Bird, it must keep working when you hand it a subclass, no surprises.
The classic violation is a Square extending Rectangle: setting width and height independently is valid for a rectangle but breaks a square's invariant, so code that trusts the Rectangle contract misbehaves. When a subclass can't honor its parent's contract, inheritance is the wrong tool, and that's what LSP catches.
Key point: Have one concrete violation ready, like Square/Rectangle or a Penguin that can't fly. A crisp example proves you understand it beyond the name.
Interface Segregation says clients shouldn't be forced to depend on methods they don't use. A fat interface with many unrelated methods makes every implementer stub out the ones that don't apply, and a change to any method touches classes that never cared about it, which is a design smell.
The fix is to split one big interface into several small, role-focused ones. A classic case: a Worker interface with work() and eat() forces a Robot to implement eat(). Split it into Workable and Eatable, and each class implements only what fits. Smaller interfaces mean less coupling and no dead methods.
// fat interface forces a Robot to implement eat()
// split into focused interfaces instead
interface Workable { void work(); }
interface Eatable { void eat(); }
class Human implements Workable, Eatable {
public void work() {}
public void eat() {}
}
class Robot implements Workable {
public void work() {} // no dead eat() method
}Key point: The smell: classes stubbing out methods they don't need. The Robot-that-can't-eat example lands the point fast.
Dependency Inversion says high-level modules shouldn't depend on low-level modules; both should depend on abstractions. In practice, a class should depend on an interface, not a concrete implementation, so you can swap the implementation, in production or in a test, without touching the class that uses it.
Take an OrderService that needs to send email. If it news up a SmtpMailer directly, it's welded to SMTP. Instead it depends on a Notifier interface, and you inject whichever implementation you want (SMTP, a mock in tests, a push service later). Inverting the dependency is what makes code testable and flexible.
interface Notifier { void send(String msg); }
class OrderService {
private final Notifier notifier; // depends on the abstraction
OrderService(Notifier notifier) { this.notifier = notifier; }
void placeOrder() { notifier.send("order placed"); }
}
// inject SmtpNotifier in prod, a fake in testsKey point: it maps to testability: injecting an interface lets you pass a fake in tests. That practical payoff is what the question really checks.
A class is a blueprint: it defines the fields and methods that its instances will have, but it holds no data of its own. An object is a concrete instance of that class, created at runtime with its own state living in memory. One class can produce many independent objects.
Think of the class Car as the design and each parked car as an object with its own color and mileage. In LLD you spend most of your time defining classes and their relationships; objects are what exist when the program runs.
All three describe relationships between classes, differing by ownership and lifetime. Association is a plain 'uses' link, like a Teacher and a Student who know each other but live independently. Aggregation is a 'has-a' where the part can outlive the whole: a Team has Players, but a Player still exists if the Team dissolves.
Composition is a stronger 'owns-a' where the part can't exist without the whole: a House has Rooms, and destroying the House destroys the Rooms. The difference matters when you decide whether one object should create and own another's lifecycle, which is a common class-diagram question.
| Relationship | Meaning | Part's lifetime |
|---|---|---|
| Association | Uses / knows about | Fully independent |
| Aggregation | Has-a (shared) | Outlives the whole |
| Composition | Owns-a (exclusive) | Dies with the whole |
Key point: The lifetime test is the shortcut: if the part dies with the whole it's composition, if it survives it's aggregation.
An interface declares a contract, the method signatures a class promises to implement, and traditionally holds no state. A class can implement many interfaces. An abstract class can hold shared state and partly-implemented methods, but a class can extend only one.
The rule of thumb: use an interface to define a capability many unrelated classes can have (Comparable, Serializable), and an abstract class to share common code and state among closely related subtypes. Interface is 'can-do', abstract class is 'is-a-kind-of' with shared implementation.
Key point: Mention that a class implements many interfaces but extends one abstract class. That single-inheritance constraint often drives the design choice.
Working code and well-designed code aren't the same thing. Code that works but tangles responsibilities gets expensive to change: a small feature touches many places, tests are hard to write, and every edit risks breaking something unrelated. LLD is what keeps a growing codebase changeable.
The payoff shows up over time: clear class boundaries mean new requirements land in new classes, bugs are easier to isolate, and teammates can reason about one piece without holding the whole system in their head. Interviewers test LLD because they're hiring for the second year of the codebase, not just the first commit.
Key point: Frame the value as 'cost of change', not 'clean for its own sake'. That's the practical lens interviewers respect.
Cohesion measures how focused a class is: high cohesion means its methods all serve one clear, related purpose. Coupling measures how dependent classes are on each other: low coupling means a change in one class rarely forces changes in the others around it.
You aim for high cohesion and low coupling. A high-cohesion class is easy to name and understand; low coupling means you can change or replace it without a ripple across the codebase. Most design principles, SOLID included, are ways to push a design toward that pair.
Key point: The target as a pair: high cohesion, low coupling. Then it connects to SOLID, since that's the common follow-up.
A UML class diagram is the standard way to sketch an object model: each box is a class with its name, fields, and methods, and lines between boxes show relationships like inheritance, association, aggregation, and composition, annotated with multiplicities such as one-to-one or one-to-many.
In an LLD interview you don't need perfect UML notation, but a clear diagram communicates your design faster than prose: which classes exist, who owns whom, and which interfaces they implement. Draw the key classes and their relationships, then talk through the important methods.
Key point: You don't need textbook-perfect UML. What scores is a readable diagram that shows classes, relationships, and multiplicities clearly.
Watch a deeper explanation
Video: UML Class Diagram Tutorial (Lucid Software, YouTube)
Compile-time polymorphism is method overloading: several methods share a name but differ in their parameters, and the compiler picks which one to call based on the arguments. Runtime polymorphism is method overriding: a subclass replaces a base class method, and the actual method that runs is chosen at runtime from the object's real type.
Runtime polymorphism is the one that matters for design, because it's what lets a caller hold a base type or interface and get the right subclass behavior without knowing the concrete class. That dynamic dispatch is the mechanism behind Strategy, the Open/Closed principle, and most extensible object models.
Key point: Interviewers care mostly about runtime polymorphism, since that's what powers extensible design. Overloading is trivia by comparison, so spend your words on overriding.
For candidates with working experience: design patterns by problem, modeling classic LLD prompts, and the judgment questions that separate people who name patterns from people who apply them.
The Gang of Four grouped patterns into three families. Creational patterns handle object creation (Factory Method, Abstract Factory, Builder, Singleton, Prototype). Structural patterns compose objects into larger structures (Adapter, Decorator, Facade, Composite, Proxy). Behavioral patterns define how objects communicate and share responsibility (Strategy, Observer, Command, State, Template Method).
Knowing the categories helps you find a pattern by the problem: 'I need flexible object creation' points to creational, 'I need to add behavior without editing the class' points to structural or behavioral. In interviews, match the problem to the family first, then pick the specific pattern.
| Category | Solves | Examples |
|---|---|---|
| Creational | How objects get created | Factory, Builder, Singleton |
| Structural | How objects compose | Adapter, Decorator, Facade |
| Behavioral | How objects interact | Strategy, Observer, Command |
Key point: Learn patterns by the problem they solve, not the name. 'Creation, structure, or interaction' is the first branch to take when picking one.
Watch a deeper explanation
Video: 7 Design Patterns EVERY Developer Should Know (ForrestKnight, YouTube)
Factory Method moves object creation behind a method so the calling code asks for an object by type or config instead of naming a concrete class with new. The factory decides which concrete class to instantiate and returns it through a common interface.
You use it when the exact class to create depends on input or context, and you want new types to plug in without editing the callers. A NotificationFactory that returns EmailNotification or SmsNotification based on a channel string lets you add a PushNotification later by extending the factory, not the code that sends notifications.
interface Notification { void send(String to, String body); }
class NotificationFactory {
Notification create(String channel) {
return switch (channel) {
case "email" -> new EmailNotification();
case "sms" -> new SmsNotification();
default -> throw new IllegalArgumentException(channel);
};
}
}Key point: Say why it helps: callers stop depending on concrete classes, so new types plug in behind the same interface. That's the Open/Closed link.
Builder constructs a complex object step by step, letting you set only the fields you need and keeping the object immutable once built. It fixes the telescoping-constructor problem, where a class with many optional fields ends up with a dozen overloaded constructors nobody can read.
Instead of new Pizza(true, false, true, 12, ...), you write a fluent chain: Pizza.builder().size(12).cheese().build(). It's readable, hard to pass arguments in the wrong order, and each optional field is explicit. Reach for it when a constructor has many parameters, several of them optional.
Pizza pizza = Pizza.builder()
.size(12)
.cheese(true)
.pepperoni(true)
.build();
// no ambiguous new Pizza(12, true, false, true, ...)Key point: The pain it removes: telescoping constructors and argument-order mistakes. That concrete framing beats 'it builds objects step by step'.
Strategy defines a family of interchangeable algorithms behind a common interface and lets you swap which one runs at runtime. The class that uses it holds a reference to the interface, not a concrete algorithm, so you can change behavior without changing that class.
A payment checkout that supports card, wallet, and cash is a natural fit: define a PaymentStrategy interface, one class per method, and let the order pick the strategy at runtime. Adding a new method means writing a new strategy class, not editing the checkout. It's the go-to pattern when you have several ways to do one thing.
interface PaymentStrategy { void pay(int amount); }
class Checkout {
private PaymentStrategy strategy;
void setStrategy(PaymentStrategy s) { this.strategy = s; }
void checkout(int amount) { strategy.pay(amount); }
}
// pass CardPayment, WalletPayment, or CashPayment at runtimeKey point: Contrast it with a switch statement on payment type. Strategy replaces that branching with polymorphism, which is the Open/Closed win to name.
Watch a deeper explanation
Video: Strategy Pattern : Design Patterns (ep 1) (Christopher Okhravi, YouTube)
Observer sets up a one-to-many dependency: a subject keeps a list of observers and notifies all of them automatically whenever its own state changes. Observers register and unregister themselves at runtime, and the subject only knows they satisfy an observer interface, never their concrete types.
It shows up anywhere one change should ripple to many listeners: UI event handlers, a stock price feeding many dashboards, a pub/sub system, or an order status that notifies email, SMS, and analytics at once. The value is decoupling: the subject stays ignorant of who's listening, so you add or remove observers freely.
interface Observer { void update(String event); }
class Subject {
private final List<Observer> observers = new ArrayList<>();
void subscribe(Observer o) { observers.add(o); }
void notifyAll(String event) {
for (Observer o : observers) o.update(event);
}
}Key point: Stress the decoupling: the subject doesn't know its observers' types. That's why it powers event and pub/sub systems, a common follow-up.
Decorator wraps an object in another object that adds behavior, and because the wrapper implements the same interface, you can stack decorators to combine features at runtime. A plain Coffee wrapped by MilkDecorator then SugarDecorator gains both without new subclasses.
Inheritance would force a class explosion: MilkCoffee, SugarCoffee, MilkSugarCoffee, one per combination, all decided at compile time. Decorator composes behavior dynamically instead, so you add one decorator class per feature and mix them freely. It's the pattern for 'add responsibilities without editing the class', the Open/Closed principle in action.
interface Coffee { double cost(); }
class MilkDecorator implements Coffee {
private final Coffee inner;
MilkDecorator(Coffee c) { this.inner = c; }
public double cost() { return inner.cost() + 0.5; }
}
// new SugarDecorator(new MilkDecorator(new Espresso()))Key point: The alternative it beats: a subclass for every feature combination. Decorator's runtime composition is the point to land.
Singleton restricts a class to a single instance and gives a global access point to it. You make the constructor private and expose a static method that returns the one instance, creating it lazily on first use. It fits genuinely shared, single things like a config registry or a connection pool.
The downsides are real: it's global mutable state in disguise, which hides dependencies (a class secretly reaches for the singleton instead of receiving it), makes unit testing hard, and needs care to be thread-safe. Many senior engineers prefer dependency injection of a single shared instance over a hard Singleton, so The trade-off, don't just praise it matters.
class Config {
private static volatile Config instance;
private Config() {}
static Config get() {
if (instance == null) {
synchronized (Config.class) {
if (instance == null) instance = new Config();
}
}
return instance;
}
}Key point: Interviewers ask Singleton to hear the downsides. Naming hidden global state and test difficulty, plus DI as an alternative, is the mature answer.
Adapter converts one interface into another that a client expects, so two otherwise incompatible interfaces can work together. It wraps the class that has the wrong interface and exposes the interface your code needs, translating each call from your shape into the wrapped object's shape in between.
It's the pattern for integrating a third-party library or legacy class you can't change: your code depends on a clean interface, and the adapter maps that to the external one. A common example is wrapping a third-party payment SDK behind your own PaymentGateway interface, so the rest of the app never touches the vendor's shape.
Key point: The keyword is 'incompatible interfaces'. Adapter is about translation, not adding behavior, which distinguishes it from Decorator.
Facade provides one simple interface in front of a complex subsystem of many classes. Instead of a caller wiring together five objects in the right order and knowing how they fit, they call one facade method that orchestrates the whole subsystem behind the scenes for them.
It's useful when a workflow touches many classes and you want callers to have an easy entry point: an OrderFacade with a placeOrder() method that internally checks inventory, charges payment, and schedules shipping. The subsystem still exists for advanced use, but the facade covers the common path and reduces coupling to the internals.
Key point: Distinguish it from Adapter: Facade simplifies a whole subsystem, Adapter converts one interface. Mixing them up is a common slip.
Start by clarifying: vehicle types (car, bike, truck), spot sizes that fit them, multiple floors, how pricing works, and how many entry and exit points. Then The entities: ParkingLot holds Floors, each Floor holds ParkingSpots, a Vehicle parks in a compatible spot, and a Ticket records entry time for pricing at exit.
For extensibility, make Vehicle and ParkingSpot small hierarchies (or use a size enum), put spot allocation behind a strategy so you can change the assignment rule, and put pricing behind its own interface so hourly, flat, or tiered pricing swaps cleanly. The scoring is about clean responsibilities and extension points, not a single 'correct' class list.
enum SpotSize { SMALL, MEDIUM, LARGE }
class ParkingSpot {
SpotSize size;
boolean occupied;
boolean canFit(Vehicle v) { return v.size().ordinal() <= size.ordinal(); }
}
class ParkingLot {
List<Floor> floors;
Ticket park(Vehicle v) { /* find spot, mark occupied, issue ticket */ }
}Modeling a parking lot: from requirements to classes
The technical judgment depends on whether you clarify before modeling and whether responsibilities land on the right class, not whether you recall a canned answer.
Key point: Clarify before you model, and put pricing and allocation behind interfaces. Interviewers steer this toward extension: 'now add electric-charging spots'.
A vending machine is the textbook case for the State pattern, because its behavior depends on which state it's in: Idle, ItemSelected, HasMoney, Dispensing. The same action (insert coin, select item) does different things in different states, and modeling that with a big if-else on a status flag gets messy fast.
Model each state as a class implementing a common VendingMachineState interface, with methods like insertCoin() and selectItem(). Each state handles the action and transitions the machine to the next state. Adding a new state or refining a transition means editing one state class, not a tangled central switch. That's the State pattern earning its place.
interface State {
void insertCoin(Machine m);
void selectItem(Machine m, String code);
void dispense(Machine m);
}
// IdleState, HasMoneyState, DispensingState each implement it
// and call m.setState(next) to transitionKey point: Reach for State and say why: behavior changes by state, so a central switch gets fragile. Naming the transitions shows you'd actually code it.
Clarify first: how many elevators, how many floors, and the scheduling goal (minimize wait time, minimize travel). Then model the entities: an ElevatorCar with a current floor, direction, and a set of target floors; a Request with a source floor and direction; and an ElevatorController that receives requests and assigns them to cars.
The interesting part is the dispatch strategy, so put it behind an interface: a simple nearest-car rule to start, with room for a smarter SCAN-style algorithm later. Keep the car's movement logic separate from the assignment logic so each can change on its own. Interviewers push on the scheduling trade-off, so be ready to discuss why you picked your rule.
Key point: Separate the scheduling strategy from the car's movement, and put dispatch behind an interface. The follow-up is always 'how do you assign requests to cars'.
Clarify the rule first: requests per user or per IP, the window, and whether bursts are allowed. Then pick an algorithm and model it as a strategy so you can swap it. Token bucket is a common default: each client has a bucket that refills at a fixed rate, a request consumes a token, and an empty bucket means reject or throttle.
Model a RateLimiter interface with an allow(clientId) method and one class per algorithm (TokenBucket, SlidingWindow, FixedWindow). Store per-client state (tokens and last-refill time) in a map, or in a shared store like Redis for a distributed limiter. Keeping the algorithm behind an interface is what makes this a clean LLD answer rather than one hardcoded rule.
interface RateLimiter { boolean allow(String clientId); }
class TokenBucket implements RateLimiter {
// per-client: tokens + lastRefill timestamp
// refill by elapsed time, consume one token per request
public boolean allow(String clientId) { /* ... */ return true; }
}Key point: The algorithm behind a strategy interface, and mention distributed state in Redis. That covers the single-node and scaled follow-ups both.
When a plain method, function, or simple class solves the problem. Patterns add indirection, and indirection you don't need makes code harder to follow, not easier. Wrapping a two-line calculation in a Strategy interface with three classes is a net loss.
The rule is to apply a pattern when the problem it solves actually exists: real variation to swap (Strategy), real need to add behavior at runtime (Decorator), real object-creation complexity (Factory or Builder). Reaching for a pattern to look sophisticated is over-engineering, and interviewers notice. The mature move is starting simple and refactoring toward a pattern when the pressure appears.
Key point: Interviewers respect 'I wouldn't use a pattern here'. Over-engineering with unnecessary patterns is a red flag, so show restraint.
An immutable object can't change after it's constructed: all its fields are final and set once, with no setters exposed. To 'change' it you build and return a new object instead of mutating the old one. Strings and value types like Money or a Coordinate are natural fits for this.
They're useful because they're safe to share: no defensive copies, no worry that some other code mutated your object, and they're inherently thread-safe since there's no state to race on. In design, making value objects immutable removes a whole class of bugs, which is why the Builder pattern often pairs with immutability to construct them cleanly.
Key point: Link immutability to thread safety and safe sharing. That practical benefit, plus 'pairs with Builder', is what makes the answer land.
Abstract Factory creates families of related objects through one interface, so a caller gets a matching set without naming concrete classes. A UIFactory that produces a Button and a Checkbox has two implementations, MacFactory and WindowsFactory, and each returns widgets that belong together. The client picks one factory and gets a consistent family.
Factory Method creates a single product through an overridable method; Abstract Factory groups several related factory methods to produce a whole family. Reach for Abstract Factory when objects must be used together and you want to guarantee they're from the same set, like a themed UI or a database driver bundle.
| Factory Method | Abstract Factory | |
|---|---|---|
| Creates | One product | A family of related products |
| Structure | One overridable method | Group of related factory methods |
| Use when | Subclass decides one type | Products must match as a set |
Key point: The distinction is 'one product versus a family that must match'. Naming a themed-UI or driver-bundle example makes the difference concrete.
An entity has an identity that persists over time, so two entities are the same only if their IDs match, even when their other fields differ. A User with id 42 is that user whether their name changes or not. A value object has no identity: it's defined entirely by its values, so two Money objects of 10 USD are interchangeable and equal.
The design consequence is how you handle equality and mutation. Value objects compare by value and are usually immutable, which makes them safe to share. Entities compare by identity and often hold mutable state tracked over their lifecycle. Getting this right shapes your equals and hashCode, and keeps a Money or a Coordinate from being accidentally treated like a database row.
Key point: The identity test decides it: same ID means same entity, same values means same value object. That framing drives equals/hashCode, a frequent follow-up.
advanced rounds probe concurrency, extensibility, and the trade-offs behind a design. Expect every answer here to draw a follow-up about failure modes and what you'd change under scale.
The naive lazy Singleton has a race: two threads can both see a null instance and each create one. The classic fixes are double-checked locking with a volatile field (check, lock, check again), eager initialization (create the instance at class load, so there's no race), or the initialization-on-demand holder idiom, where a static inner class holds the instance and the JVM guarantees safe lazy loading.
In many languages the cleanest option is a language feature: a Java enum Singleton is thread-safe and serialization-safe for free. The senior point is knowing why the naive version breaks and picking the option that fits, rather than reflexively wrapping everything in a synchronized block, which serializes every access.
// holder idiom: lazy and thread-safe without locking on every call
class Config {
private Config() {}
private static class Holder { static final Config INSTANCE = new Config(); }
static Config get() { return Holder.INSTANCE; }
}Key point: Explain the race first, then the fix. Knowing the holder idiom or enum singleton over a blanket synchronized method signals real concurrency depth.
Start by finding the shared mutable state, because that's the only thing that needs protecting. Then choose the lightest tool that's correct: immutable objects need no locking at all, atomic variables handle simple counters, concurrent collections handle shared maps and queues, and explicit locks are the fallback when you must guard a compound operation.
The trade-offs to name: locks are correct but serialize access and risk deadlock if you take them in inconsistent order, so keep critical sections small and lock ordering consistent. Prefer designs that avoid shared mutable state in the first place (message passing, per-thread state, immutability) because the safest lock is the one you never need.
Key point: Lead with 'minimize shared mutable state', then pick tools by weight. Reaching straight for a big synchronized block indicates junior under concurrency questions.
The core is a bounded buffer shared between producer threads that add work and consumer threads that remove it. Producers block when the buffer is full, consumers block when it's empty, and you need coordination so no item is lost or processed twice. A blocking queue handles all of that: put() blocks on full, take() blocks on empty, and the queue manages the locking internally.
If you build it by hand, you guard the buffer with a lock and use two condition variables (notFull and notEmpty) so producers wait on one and consumers on the other, signaling across as items move. Getting the wait/notify logic right, and using a loop to recheck the condition after waking, is the part interviewers probe, because spurious wakeups and lost signals are the classic bugs.
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(100);
// producer
queue.put(task); // blocks if full
// consumer
Task t = queue.take(); // blocks if emptyKey point: Reach for a BlockingQueue first, then show you could build it with a lock and two conditions. The rechecking-in-a-loop detail proves you've written it.
Find the axis of change and put an abstraction across it. If new payment methods keep arriving, a PaymentMethod interface absorbs them; if pricing rules change, a pricing strategy does. The goal is that a new requirement lands as a new class implementing an existing interface, which is the Open/Closed principle applied deliberately.
You don't abstract everything up front, though, because guessing wrong adds indirection you'll never use. The pragmatic path is to design simply, then, when a second variation appears, refactor to an abstraction across exactly that axis. Naming the likely axes of change and defending which ones you'd abstract now versus later is the senior-level answer.
Key point: Talk about 'axes of change' and abstracting only where variation is real. Balancing extensibility against over-engineering is exactly what this screens.
Put a Channel interface at the center with a send(user, message) method, and implement one class per channel: email, SMS, push, in-app. The code that decides to notify someone depends only on the interface, so adding WhatsApp later means writing a WhatsAppChannel, not editing the dispatcher. That's Dependency Inversion and Open/Closed together.
Layer in the real requirements: a factory or user-preference lookup to choose channels, the Observer pattern so one event fans out to multiple channels at once, retries and a fallback channel for failures, and templating so message content is separate from delivery. Keeping delivery, selection, and content as separate responsibilities is what keeps the system changeable as channels and rules multiply.
interface Channel { void send(User user, String message); }
class Notifier {
private final List<Channel> channels;
Notifier(List<Channel> channels) { this.channels = channels; }
void notify(User user, String message) {
for (Channel c : channels) c.send(user, message);
}
}Designing an extensible notification system
Each new channel is a new class, never an edit to the dispatch code. That's Open/Closed and Dependency Inversion working together.
Key point: Separate delivery, selection, and content into distinct responsibilities. Interviewers extend this with retries and fallback, so mention failure handling early.
Command turns a request into an object: it wraps an action and its arguments behind an execute() method, so the code that triggers the action is decoupled from the code that performs it. Because each command is an object, you can queue them, log them, or store them.
It earns its place when you need undo/redo (each command also implements undo()), a task queue, or a history of operations, think a text editor's edit stack or a job scheduler. For a plain method call it's overkill, but for anything that treats operations as first-class things you store and replay, Command is the right tool.
interface Command { void execute(); void undo(); }
class AddTextCommand implements Command {
// holds the text and position
public void execute() { /* insert text */ }
public void undo() { /* remove it */ }
}
// a history stack of commands enables undo/redoKey point: Justify it with undo/redo or a queue, not just 'it wraps a request'. Command without one of those use cases is over-engineering.
Template Method defines the skeleton of an algorithm in a base class method, with some steps fixed and others left as abstract hooks that subclasses fill in. The overall flow is locked; only the varying steps are overridden. It's the inheritance-based cousin of Strategy.
A data-processing pipeline is a natural fit: the base class defines readData(), then processData(), then writeResults() in fixed order, and each subclass implements the specific read and write for its format. You use it when the algorithm's The technical sequence is constant but individual steps differ. The trade-off versus Strategy is that Template Method uses inheritance, so it's less flexible at runtime.
Key point: Contrast it with Strategy: Template Method varies steps via inheritance, Strategy swaps whole algorithms via composition. Naming that trade-off is the production signal.
Dependency injection means a class receives its collaborators from outside (through the constructor, usually) instead of creating them itself. So an OrderService is handed a PaymentGateway rather than newing one up. It's the practical mechanism that makes Dependency Inversion real.
It improves design by decoupling: the class depends on an interface it's given, so you swap implementations without touching it, pass fakes in tests, and see a class's dependencies plainly in its constructor. The alternative, a class reaching out to create or look up its own dependencies, hides those dependencies and welds the class to concrete types.
class OrderService {
private final PaymentGateway gateway;
// dependency injected via constructor
OrderService(PaymentGateway gateway) { this.gateway = gateway; }
}
// prod: new OrderService(new StripeGateway());
// test: new OrderService(new FakeGateway());Key point: DI maps to visible dependencies and testability. Contrasting constructor injection with a class that news up its own collaborators makes the benefit concrete.
Decide what's exceptional versus expected, and model them differently. Truly exceptional conditions (a corrupt state, a missing dependency) throw; expected outcomes (a lookup that finds nothing, a validation that fails) are better returned as a value, an Optional, a result type, or a domain object, so callers must handle them and don't rely on catching.
Design a small hierarchy of meaningful exception types instead of one generic Exception, so callers can catch what they can handle and let the rest propagate. Fail fast on programming errors, don't swallow exceptions silently, and include enough context in the error to diagnose it. The design goal is that a caller can tell recoverable from unrecoverable without reading your implementation.
Key point: Separate 'expected outcome' from 'exceptional'. Returning a result for expected cases and reserving exceptions for the truly abnormal is the mature distinction.
Composite lets you treat individual objects and groups of objects uniformly by giving both the same interface. A tree structure where leaves and containers share one interface means client code can call the same method on either without checking which it is.
It fits any part-whole hierarchy: a filesystem where a File and a Folder both have size() and the folder sums its children; a UI where a Button and a Panel both render(); an org chart. The win is that recursive operations become simple, no special-casing leaves versus containers, because they answer the same interface.
interface Node { int size(); }
class File implements Node {
int bytes;
public int size() { return bytes; }
}
class Folder implements Node {
List<Node> children = new ArrayList<>();
public int size() {
return children.stream().mapToInt(Node::size).sum();
}
}Key point: The keyword is 'part-whole hierarchy' where clients treat leaf and group the same. Filesystem or UI tree examples make it concrete fast.
Proxy puts a stand-in object in front of a real object, sharing its interface, to control access to it. Callers talk to the proxy as if it were the real thing, and the proxy adds behavior before or after delegating to the real object.
The common variants map to real needs: a virtual proxy delays creating an expensive object until it's first used (lazy loading), a protection proxy checks permissions before allowing a call, a remote proxy represents an object living on another machine, and a caching proxy returns stored results. It's the pattern for 'wrap access without changing the caller or the real class'.
Key point: List the variants by their purpose: lazy loading, access control, remote, caching. Mapping each to a use case beats a single vague definition.
The requirement is get and put in O(1) with a capacity, evicting the least-recently-used entry when full. The standard The technical sequence is a hash map for O(1) lookup combined with a doubly linked list that tracks usage order: the most recently used at the head, the least at the tail. The map stores keys to list nodes so you can find and move a node in constant time.
On get, you look up the node and move it to the head. On put, you insert or update, move to the head, and if you're over capacity, remove the tail node and its map entry. The doubly linked list is what makes eviction and reordering O(1); a plain list or array would make one of the operations linear. In Java, a LinkedHashMap with access order gives you this almost for free.
// map: key -> node; doubly linked list: usage order
class LRUCache {
Map<Integer, Node> map = new HashMap<>();
// head = most recent, tail = least recent
int get(int key) { /* move node to head, return value */ }
void put(int key, int val) { /* insert/update, evict tail if full */ }
}Key point: Say 'hash map plus doubly linked list for O(1)' up front, then walk the get/put steps. That data-structure choice is the whole point of the question.
Design for the requirements you actually have, not the ones you imagine. Every abstraction, interface, and pattern is a bet that a certain change will come; if it doesn't, you've paid the indirection cost for nothing and made the code harder to read. The default should be the simplest thing that satisfies the current, clarified requirements.
The discipline is YAGNI (you aren't gonna need it) plus refactoring: build simple, and when a real second case or a real pain point appears, introduce the abstraction across exactly that axis. Being able to say 'I'd a plain class and extract an interface once a second implementation shows up' is what separates a senior from someone pattern-matching for its own sake comes first.
Key point: Cite YAGNI and 'refactor when the second case appears'. Interviewers plant chances to over-abstract; resisting them is the signal here.
Treat the challenge as a real design review, not an attack. Restate the requirement your choice serves, The alternatives you considered, and explain the trade-off you made, for example 'I used composition here because the behaviors need to combine at runtime; inheritance would have forced a class per combination.' Grounding the decision in a requirement is far stronger than defending it by preference.
When the interviewer raises a case you didn't handle, say so and adapt: 'good point, if we need X too, I'd extract this into a strategy.' Showing you can revise under new information indicates senior, whereas rigidly defending a first draft indicates inflexible. The goal is demonstrating judgment and how you reason, not proving the first sketch was perfect.
Key point: Justify choices by the requirement and the rejected alternative, and adapt when a new constraint appears. Flexibility under challenge is exactly what advanced rounds test.
The right structure often decides whether a design meets its performance requirement. A hash map gives average O(1) lookup by key, which is why it backs caches and indexes. A balanced tree gives O(log n) lookup but keeps entries sorted, so you use it when you also need range queries. A plain list is O(n) to search, fine only for small collections.
In an LLD round, The access pattern first, then the structure: frequent lookups by key point to a map, ordered access points to a tree or sorted structure, FIFO work points to a queue. Choosing a structure because it fits the operations, not out of habit, is what turns a correct design into an efficient one.
Average-case lookup cost by data structure
Big-O for a single lookup by key. Lower is faster. A hash map's O(1) is why it backs most in-memory indexes; a sorted structure trades speed for order.
Key point: Justify each structure by the access pattern, not habit. Saying 'O(1) lookups, so a hash map; ordered range queries, so a tree' is the reasoning the question needs.
The Law of Demeter, the principle of least knowledge, says a method should only talk to its immediate collaborators: its own fields, its parameters, and objects it creates, not the objects those return. The smell it catches is a chain like order.getCustomer().getAddress().getCity(), which reaches deep through several objects.
Long chains couple your class to the internal structure of others, so a change three objects away breaks you. Following the law, you ask the immediate object to do the work (order.getShippingCity()) instead of digging through its internals. It matters because it keeps coupling low and makes refactoring safe, though a rigid reading can add pass-through methods, so apply it with judgment.
Key point: The train-wreck chain (a.getB().getC().getD()) as the smell, and the fix of asking the immediate object. Acknowledging the pass-through cost shows balanced judgment.
Candidates confuse LLD with the two rounds next to it. High level design (HLD) works at the system level: which services exist, which database, how they scale. LLD works inside one service: the classes, their methods, and how objects talk. The algorithm round is different again: it optimizes a single function's time and space, not the shape of a class model. Knowing which round you're in, and answering at that altitude, is itself a signal. In an LLD round the question needs a class diagram and extensible code, not a boxes-and-arrows deployment map and not a clever one-liner.
| Aspect | High level design | Low level design | Algorithm round |
|---|---|---|---|
| Scope | Whole system, many services | One service or module | One function |
| Unit of work | Services, databases, queues | Classes, interfaces, methods | Data structures, loops |
| Main skill | Scaling and trade-offs | OOP, SOLID, patterns | Time and space complexity |
| Typical output | Architecture diagram | Class diagram plus code | Optimized code and Big-O |
| Example prompt | Design a URL shortener at scale | Design a parking lot's classes | Reverse a linked list |
Prepare in layers, and practice talking through a design out loud. Most LLD rounds move from clarifying the problem to sketching classes to writing code to defending trade-offs, so rehearse each stage rather than only reading answers. The candidates who struggle jump straight to classes without pinning requirements first.
The typical low level design interview flow
Earlier rounds increasingly run as AI-driven technical interviews. The fundamentals on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These Low Level Design questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works