Low-Level Design & Object-Oriented Design
Low-level design is the round where you produce classes, interfaces and interactions inside one service rather than infrastructure boxes. It scores where you put boundaries, what you name things, how the design absorbs the next requirement, and whether the code runs before the timer stops.
Assumes you know: Fluency in one object-oriented language you can write without an IDE prompting you, Comfort defining a class, an interface and a unit test unaided, Having read someone else's codebase and had to extend it
Overview
What this area actually covers
Low-level design, usually written LLD and sometimes called object-oriented design, is the design of the inside of one program. You are given a problem small enough to hold in your head — a parking lot, a lift bank, a vending machine, a split-the-bill app, a cache with expiry — and asked to produce the types: which classes exist, what each owns, what the interfaces between them look like, and what happens as a request travels through them. The deliverable is a class diagram with method signatures, or in a machine-coding round, working code.
The subject matter is class boundaries and the vocabulary for discussing them:
encapsulation, which is about which invariants a class is responsible for rather than
about writing getters; polymorphism, which is how you delete a switch that grows every
quarter; composition versus inheritance, a decision you justify from the requirements
rather than from a slogan; cohesion and coupling, which every other heuristic here is a
proxy for; SOLID, useful as diagnostic questions and misleading as rules; design
patterns, which are names for shapes that recur; and concurrency within one component,
meaning what is immutable, what is guarded, and by which lock.
Two things get wrongly bundled in. Distributed architecture — sharding, replication, queues, regions — belongs to the system design round; LLD lives inside one process, and if your answer to "two people book the same seat" is "Kafka", you have left the room you were asked to design. Algorithmic problem-solving belongs to the DSA round: machine-coding problems are deliberately shallow there because the difficulty is meant to be structural, and spending twenty of your ninety minutes optimising a lookup over four elements is a misread of the exercise.
The seven topics underneath
The section splits into seven topics that form a deliberate progression. The first two are the reasoning tools, the next three are the shared vocabulary, and the last two are the places the reasoning gets tested under conditions that resemble the job.
| Topic | What it is for |
|---|---|
| OOP Principles | The four ideas every other topic here is built from |
| SOLID & Design Heuristics | Diagnostic questions for deciding where a boundary goes |
| Creational Patterns | Controlling how objects come into existence |
| Structural Patterns | Composing objects so a change lands in one place |
| Behavioural Patterns | Assigning responsibility for a decision or a sequence |
| Concurrency in Design | Keeping a component correct when two threads use it |
| Machine Coding Rounds | Doing all of the above against a clock, in running code |
OOP Principles is the foundation, and it is worth relearning even if you think you know it, because the interview definitions are more demanding than the textbook ones. Encapsulation is not private fields with accessors; it is a class taking responsibility for an invariant so that no caller can put it in an invalid state. Polymorphism is not a language feature to name; it is the mechanism by which a conditional that grows with every requirement becomes a set of types that does not. Inheritance versus composition is where most bad designs originate, and the topic covers when a subclass genuinely substitutes for its parent and when you have modelled "has a" as "is a".
SOLID & Design Heuristics takes the five principles and treats them as questions rather than laws, which is the only way they are useful. Does this class have one reason to change? Can I add behaviour without editing existing code? Would a caller be surprised if I substituted this subtype? Am I forcing implementers to provide methods they do not need? Do the details depend on the abstraction, or the other way round? The topic also covers the two properties the five principles are proxies for — high cohesion inside a unit and low coupling between units — because when the principles conflict, and they do, cohesion and coupling are the tiebreaker.
Creational Patterns deal with a narrow but recurring problem: object construction that has become complicated enough to deserve its own home. Factory methods when the concrete type depends on input the caller should not know about, builders when a constructor has too many parameters and half of them are optional, prototypes when copying an existing object is cheaper than building one. Singleton is here too, and the topic is honest that it is the pattern most often applied where it does harm, because it is global mutable state with a respectable name.
Structural Patterns are about composing objects so that a change arrives in one place. Adapter when you must not let a vendor's types leak into your code, decorator when behaviour stacks in combinations you cannot enumerate in advance, facade when a subsystem is correct but unpleasant to use, proxy when access needs a gate, composite when a leaf and a group of leaves should be treated identically. Each one exists as an answer to a specific pressure, and the topic is organised around the pressure rather than around the diagram.
Behavioural Patterns allocate responsibility for a decision or a sequence. Strategy when an algorithm varies independently of the thing using it, observer when one change must notify an unknown number of interested parties, state when an object's legal operations depend on which mode it is in, command when an action must be queued, logged or undone, template method when the skeleton is fixed and the steps vary. These are the patterns that appear most often in interview problems, because interview problems are usually about behaviour that varies.
Concurrency in Design is the topic that separates a design that works in a demonstration from one that would survive a production request rate. It covers what a thread-safe component actually promises, the choice between guarding shared state and eliminating it through immutability, lock granularity and the throughput cost of getting it wrong, and the atomicity trap where two individually safe calls make an unsafe sequence. In an interview it usually arrives as a follow-up rather than the main prompt, which is why it is easy to be caught out by.
Machine Coding Rounds are where everything above is graded together under a clock, typically sixty to a hundred and twenty minutes to build a runnable console application from a written brief. Parking lot, lift bank, vending machine, a bill-splitting app, a cache with eviction and expiry — the problems recur, and the topic covers both the recurring problems and the scheduling discipline that separates a submission that runs from one that does not. It is the most format-specific topic here and the one that most rewards timed rehearsal.
Where it sits in a real system
Inside any single process there is a layer that accepts requests and validates their shape, a layer holding the rules of the business, and a layer that talks to a database, a queue or a vendor's API. Low-level design is where those lines fall, which direction dependencies cross them, and which types may appear on both sides. That is the difference between a service where a new payment method is one new class plus a configuration entry, and one where it is a fortnight of touching seventeen files.
flowchart TD
A[Transport layer accepts a request] --> B[Validate shape and map to a domain type]
B --> C[Domain object enforces its own invariants]
C --> D[Service coordinates the steps of the use case]
D --> E[Port interface defined by your code]
E --> F[Adapter implements it using the vendor SDK]The arrow to watch is the one from D to E. The service depends on an interface your codebase owns, and the adapter that knows about the vendor sits on the far side of it, so replacing the vendor changes one class. Reverse that arrow — let the service call the SDK directly — and the vendor's types spread through your domain, which is the single most common structural defect in real services and the one interviewers probe with "the SMS provider is being replaced next quarter".
The pressure is always the same: something the requirements called fixed turns out to vary — a second currency, a second tax jurisdiction, a discount applying to some line items only. A design is good exactly to the extent that such a change lands in one place, and every classic interview problem contains one of these hinges. The same problem at the two altitudes shows what each round grades:
| Question | System design answers | Low-level design answers |
|---|---|---|
| Two users book the last seat | Where the write is serialised, and which store gives that guarantee | Which object owns the seat's state, and what its method returns on failure |
| A third pricing rule arrives | Nothing — not an architectural event | Whether it is a new class behind an existing interface, or an edit inside a 200-line method |
| The SMS vendor changes | Whether the call is synchronous or queued | Whether your code depends on your interface or on the vendor's SDK types |
| Reads are 50x writes | Cache, replicas, CDN | Whether the read path can take an immutable snapshot without a lock |
Both columns are real work, graded by different people. The mistake worth avoiding is assuming the left column is the senior one. A distributed system made of badly factored services is harder to change than a well-factored monolith, and the factoring is decided entirely in the right-hand column.
Who does this work
Every engineer who ships production code does this daily, named or not. It becomes an explicit responsibility for senior engineers and tech leads, who argue about exactly these boundaries in code review and who write the first version of a module so the shape is set before five people build on it. Mobile engineers do disproportionately much of it, because an app is one large process with no network boundary to hide behind, which is why Android and iOS loops weight this round heavily.
A day looks like this: a ticket arrives asking for behaviour the current types cannot express cleanly, and you decide whether to bend the model, extract an interface, or accept the ugly version because the deadline is real and the ugliness is contained. Then you defend that in review. The interview is that conversation, compressed.
There is a second population worth distinguishing, because their incentives differ. Library and platform authors inside a company design types that other teams consume and cannot easily change, so their cost of getting a boundary wrong is measured in migrations rather than in one refactor. They over-invest in the interface relative to a product team, and correctly so. If you are interviewing for a platform or SDK role, expect the questions to push harder on API surface, versioning and what happens to callers when you change your mind — and expect a design that a product engineer would call over-abstracted to be the right answer there.
Demand, adoption and how that is changing
Demand is high, with an important qualifier: it is unevenly distributed by market. In Indian product-company hiring this is a standard and often decisive round — a large share of loops at product firms and funded startups include an explicit LLD discussion or a timed machine-coding round of sixty to a hundred and twenty minutes in which you build a runnable console application. Service and consultancy hiring leans more towards language and framework concepts. Much of US and European hiring assesses the same skill informally, folded into a coding round with a follow-on "now add this requirement", or into a code review exercise onsite. So calibrate to where you are applying: interviewing in Bengaluru, Hyderabad, Pune or the NCR, treat machine coding as a first-class round and practise against a clock.
The force sustaining demand is unglamorous. Assistants now produce a plausible implementation of a small problem in seconds, which devalues generating syntax and raises the value of judgement about structure. Interviewers have responded by pushing on extension rather than production: expect a new requirement fifteen minutes before the end, and expect to be scored on what your design costs to accommodate it.
The same force is changing the format in a second way. Where a machine-coding round once tested whether you could produce a hundred and fifty lines of correct code in an hour, several companies now hand you a working implementation and ask you to extend or repair it. That is a harder exercise, because you cannot impose your own structure — you have to read someone else's, infer the boundaries they intended, and add behaviour in a way that respects them. It also happens to resemble the job much more closely, which is why the shift is likely to continue.
How the round is actually run
The two formats differ enough that preparing for one leaves you underprepared for the other, and candidates routinely discover which they are in only after it starts.
The discussion round is forty-five to sixty minutes on a shared document or whiteboard. You are given a one-paragraph brief, and the expected opening is not a class diagram but questions: what is in scope, what varies, who the actors are, what happens in the failure cases. Fifteen minutes of clarification followed by a clean model beats forty minutes of drawing that answered the wrong problem. The interviewer will interrupt — treat every interruption as a hint rather than an attack, because it is nearly always pointing at something they intend to grade later. The deliverable is class names, responsibilities, the key method signatures, and a walkthrough of one request travelling through them.
The machine-coding round is sixty to a hundred and twenty minutes producing code
that runs. There is usually no database, no framework, no network and no user interface;
state lives in memory and the demonstration is a main method or a small set of tests
that exercise the scenarios in the brief. What is graded is the structure, whether the
required scenarios work, and whether the code is readable — not cleverness, and
emphatically not completeness at the expense of running. Many briefs contain more
requirements than anyone finishes; choosing which to drop, saying so, and shipping the
rest working is a better outcome than attempting all of them and delivering none.
Both formats end the same way, with an extension. The interviewer adds a requirement in the last ten minutes and watches what you do. In the discussion round you say which classes change; in the machine-coding round you may be asked to implement it. Preparing for that moment specifically — by inventing an extension for every problem you practise and tracing it — is the highest-yield thing you can do for this section.
What makes it hard
The problem is small enough that everyone produces something, so the score comes from discrimination between designs rather than from arriving at an answer. Everyone who reaches the round can model a parking lot. Whether you noticed that the pricing rule varies along a different axis from the spot type, and whether you can say why you did not introduce an interface for the thing with exactly one implementation, is the assessment.
The good instincts are learnt from consequence. Over-abstraction and under-abstraction both hurt, and reading cannot tell you which you are doing: you learn that a factory returning one type is dead weight by having maintained one.
Time is the third difficulty, and it is a scheduling problem as much as a design one.
Candidates fail machine coding in two symmetrical ways: designing beautifully on paper
and handing in something that does not run, or writing a 400-line main that runs and
demonstrates nothing. Both are avoidable only by having done it under a clock before.
Fourth, patterns are a vocabulary rather than a checklist. Knowing the word "decorator" lets you describe a structure in three words instead of three sentences, which is genuinely useful. It does not tell you that this problem wants one. The classic weak answer, which interviewers hear constantly, opens by announcing which patterns will be used and then bends the problem to fit: a singleton for the inventory, a factory for the vehicles, an observer for something. Delivered as a list it signals a rehearsed catalogue rather than a designed solution, and it scores worse than a plain answer containing no pattern names at all, because the interviewer must now spend the round establishing whether you understand any of them.
Fifth, and least expected by people who prepare thoroughly, naming is graded. A reader
of your class diagram forms a model of the domain from the nouns you chose, and a
design full of Manager, Handler, Processor and Helper tells them you did not
decide what those objects are responsible for. The reliable test is whether you can
describe a class's job in one sentence without the word "and"; if you cannot, the name
is vague because the responsibility is.
Two heuristics that actually discriminate
Most of the published guidance on this subject reduces to slogans, and slogans do not help under time pressure. Two heuristics do, because both are checkable in seconds against a design you have just drawn.
The first is axes of change. Ask what varies independently of what. In a parking lot, the vehicle type varies, the pricing scheme varies, and the way a spot is allocated varies — and crucially they vary independently of each other, because a new pricing scheme should not require touching vehicle types. Every independent axis of change is a candidate for its own abstraction, and every abstraction that does not correspond to an axis is decoration. This single question resolves most over-abstraction arguments, because it converts "should I add an interface here" into "will this vary separately from that", which has an answer in the requirements.
The second is the cost of the next requirement. Before you present a design, invent the requirement you would least like to receive and trace it. If it lands in one new class implementing an interface that already exists, the design is doing its job. If it means editing a conditional in three files, you have found the seam you failed to create. This is exactly what the interviewer will do at the end of the round, so doing it first turns their hardest question into something you have already answered.
A third question is worth asking when the first two disagree, and it is about where knowledge lives. For any rule in the system, ask which object knows it. If the rule that a spot fits a vehicle lives in the allocator rather than in the spot, then every future caller of the spot has to remember to apply it, and one of them eventually will not. Pushing a rule down to the object that owns the data it depends on is what encapsulation means operationally, and it is a more useful formulation than any definition of the word, because it produces a concrete edit rather than a feeling.
Both heuristics point at the same underlying property: the design should be open to the variation you can foresee and should not have paid for variation you cannot. Paying in advance for flexibility you never use is a real cost — one more indirection for every reader, one more file to open, one more thing that must be kept consistent — and the reason experienced designers under-abstract relative to enthusiastic ones is that they have paid that cost and remember it.
A design worked through
Take the vending machine, the standard first problem, and follow just the part that
discriminates. A naive design puts everything on one class with a status string and a
method for each button. It works, and it fails the extension test immediately: adding
card payment means editing a conditional that already handles coins, and the rules
about which operations are legal when are spread across every method.
The alternative is to notice that the machine is a state machine, and that "which operations are legal" is the state's responsibility rather than a check at the top of each method.
stateDiagram-v2
[*] --> Idle
Idle --> Collecting: item selected
Collecting --> Dispensing: enough money received
Collecting --> Refunding: cancelled
Dispensing --> Idle: item and change released
Refunding --> Idle: money returnedThe transition that carries the design decision is Collecting to Refunding. If cancel is legal only from Collecting, the state object for every other state simply does not offer it, and the illegal case cannot be reached rather than being rejected by a guard clause you might forget to write. That is the difference between encoding rules in types and checking them in conditionals, and it is what an interviewer means when they ask you to make invalid states unrepresentable.
The payment axis is separate, and separating it is the second decision:
// The machine depends on this, not on any concrete payment type.
interface PaymentMethod {
boolean collect(Money amount);
Money refund(); // cards refund the whole amount; coins return change
}
// Adding cards is a new class. Nothing in the state machine changes,
// because payment varies along a different axis from machine state.
final class CardPayment implements PaymentMethod { /* ... */ }
Two axes, two abstractions, and neither one is a pattern name you had to announce. If you were asked afterwards, you would say the states are the state pattern and the payments are strategy — but the design came from the axes, and the names came afterwards. That order is what the round is trying to detect.
Concurrency inside one component
The follow-up that ends more LLD rounds than any other is some form of "now two threads use this at once". It is asked because it is the cheapest way to find out whether a candidate's design is a drawing or a program, and because the honest answer requires you to have thought about state ownership rather than about classes.
Start from the observation that a design has no concurrency problem where it has no shared mutable state. An object created per request, or an object whose fields are set once at construction and never changed, is safe by construction and needs no lock and no discussion. That is why the first move of an experienced designer is not to add synchronisation but to shrink the surface that needs it — make the value objects immutable, keep per-request working state on the stack, and reduce the shared part to the smallest thing that genuinely must be shared, which in most interview problems is a single collection of inventory, seats or accounts.
What remains has to be guarded, and the interesting question is by what and at which granularity. One lock over the whole component is easy to explain and correct, and it serialises every operation including reads that could have proceeded together. A lock per entity — per parking spot, per seat, per account — preserves parallelism and introduces the possibility of a deadlock the moment any operation needs two of them, which is why an operation that touches two accounts must acquire their locks in a fixed global order rather than in the order the caller happened to name them. Saying that sentence unprompted is worth more in this round than naming three patterns.
The second trap is atomicity across calls. A component can offer two individually
thread-safe methods and still be unusable, because the caller has to check something and
then act on it, and another thread gets in between. A concurrent map that offers a
thread-safe get and a thread-safe put does not let you safely implement "insert if
absent" out of the two; you need a single operation that does both, which is why such
maps provide one. The general form of the rule is that thread safety is a property of an
interface, not of its individual methods, and the interface has to expose whatever
compound operation the caller actually needs.
The third is the one that only appears under load: a lock is a queue, and every thread waiting on it is capacity you paid for and are not using. That converts a correctness decision into a throughput decision, and it is why the strongest answers to a contention follow-up move state rather than guarding it — partitioning the collection so different keys contend on different locks, taking an immutable snapshot for the read path so readers never block writers, or replacing a read-modify-write with a single atomic operation. None of that requires distributed systems vocabulary, which is the point: it is all inside one process, which is where this round lives.
Why study it
Study it if you are interviewing in a market where the round exists, because it is heavily weighted and a fortnight of the right practice moves the score a lot. Study it if you are aiming at senior, because "sets the shape of a module others build on" is a promotion criterion at most companies and this is that skill renamed. And study it because the returns transfer immediately to your pull requests.
There is a less obvious reason, which is that this is the cheapest available training in reading code. Almost all of the judgement here is about what a future reader will infer from your structure, and the fastest way to develop it is to alternate between designing something and extending someone else's design. Engineers who are good at this are usually the ones who have had to work inside a codebase they did not write and were paying attention while they did.
Two honest exclusions. For data, ML, SRE and infrastructure roles this round is frequently absent, and your effort belongs in domain-shaped design and coding practice. And if you already write code that colleagues extend without complaining, a pattern catalogue will tell you little you do not do by instinct; your gap is more likely articulation under time pressure, which is a different exercise.
Your first hour
Do not open a pattern catalogue. Pick one problem — a vending machine is the right size
— set a timer for sixty minutes, and write code that compiles and runs a scenario from
main.
Constrain it so the design question is unavoidable: three products at different prices, three coin denominations, correct change, an out-of-stock path, a refund on cancel. Sketch the types on paper for five minutes, then implement.
Budget the hour before you start, because the scheduling is half the exercise. Five
minutes on types, forty on implementation, ten on making a scenario run end to end from
main, and five spare. The rule that saves most submissions is to get something running
early and keep it running: a machine that vends one product correctly at minute thirty
and grows from there always beats an elegant half-finished model that has never
executed.
When the timer stops, do the part most people skip. Give yourself a new requirement — the machine now takes cards, and cards need no change — and answer in writing:
Which files must I open to add card payment?
Which of those are edits to existing logic rather than new code?
Where did I add an interface that has exactly one implementation and always will?
Where is there a switch or if-chain that grows with every future requirement?
What would I name differently now that I have written the whole thing?
You finish with a runnable program and five honest answers, which is better preparation than any chapter you could have read instead. Then repeat with a second problem of a different shape — a lift bank forces state machines and scheduling — and a third. Two or three problems taken all the way to running code and interrogated like this beat fifty pages on patterns.
What this is not
It is not the pattern catalogue. Patterns are shared vocabulary for describing a structure quickly; treated as a menu to apply, they are the most reliable route to a bad design and a worse interview.
It is not system design, and confusing them costs marks both ways — load balancers in answer to an LLD prompt, or class diagrams in answer to a system design prompt, both read as not having understood the question.
It is not UML for its own sake. Sketch classes and relationships clearly enough that someone follows you; no interviewer has ever cared about the arrowhead convention for aggregation versus composition. Nor is it competitive programming: the algorithms in these problems are trivial by design.
It is not the same as knowing an object-oriented language well, either, and the gap surprises experienced people. You can be completely fluent in a language's type system and still produce a design where responsibilities are in the wrong places, because fluency is about expressing a decision and this round is about making one.
Nor is it a claim that object orientation is the only way to organise a program. Plenty of excellent systems are written in a largely functional or data-oriented style, and the underlying questions — what varies, who owns which invariant, how a change propagates — survive the translation intact. What the round tests is the reasoning; objects are simply the notation most interviewers share.
And it is not clean code as an aesthetic. Every heuristic here exists to make the next change cheap, and a refactoring that leaves the code prettier and the next change no cheaper did not earn its diff.
The round comes down to a question asked near the end: here is a new requirement, what does it cost you? Design so the answer is "one new class", and be ready to say why you did not abstract the parts that were never going to vary.
Where to go next
Now practise it
12 interview questions in Low-Level Design & OOD, each with the rubric the interviewer is scoring against.
- Adapter, decorator and facade - what concrete problem does each one solve, and where would reaching for it be over-engineering?
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?
- Where does encapsulation or polymorphism change a design, and how do you decide between composition and inheritance?
- When is a factory the right answer, when is a builder, and when should you just call the constructor?