What low-level design is
Low-level design (LLD) is the work of turning a feature into concrete classes, interfaces, methods and the relationships between them. If someone says "build a parking lot system", LLD answers questions like: which classes exist (ParkingLot, Floor, Spot, Ticket)? What does each one know and do? Which methods does a caller use? How do you add a new vehicle type next month without rewriting half the code?
You will hear the same round called by three names:
- Object-oriented design (OOD) — the emphasis is on modelling a problem with objects, classes and interfaces.
- Machine coding — the emphasis is on writing working, runnable code within a fixed time, usually 60 to 120 minutes.
- LLD — the umbrella term used in most Indian product-company interview loops; it covers both.
LLD matters in interviews because it is the closest thing to a sample of your daily work. A data-structures round shows that you can solve a puzzle; an LLD round shows whether your code would be pleasant for a teammate to read, extend and test six months later. Interviewers typically probe four things: whether you ask good questions before coding, whether your classes have clear responsibilities, whether your design survives a new requirement thrown at you halfway through, and whether the code actually runs.
This lesson is the map for the whole track. You will learn how LLD differs from high-level design, the two interview formats you will meet, exactly what interviewers score, and a step-by-step framework with a time budget. You will then apply the framework to a small example from start to finish.
LLD vs HLD
High-level design (HLD), also called system design, works at the level of services, databases, caches, queues and networks. It asks "how do we serve 50 million users?" LLD works inside one service. It asks "how is the code inside this service organised?"
A useful mental picture: HLD draws boxes for services and arrows for network calls. LLD opens one of those boxes and draws the classes inside.
HLD view (system) LLD view (inside one box)
+--------+ +-----------+ +-------------+ +-----------+
| Client |--->| Booking | | BookingSvc |-->| SeatLock |
+--------+ | Service | +-------------+ +-----------+
+-----+-----+ |
| v
+-----v-----+ +-------------+ +-----------+
| Postgres | | Booking |-->| Payment |
+-----------+ | (entity) | | Strategy |
+-------------+ +-----------+
| Aspect | Low-level design | High-level design |
|---|---|---|
| Question it answers | How is the code structured? | How is the system structured? |
| Building blocks | Classes, interfaces, enums, methods | Services, databases, caches, queues, load balancers |
| Main concerns | Readability, extensibility, correctness, testability | Scalability, availability, latency, consistency |
| Typical output | Class diagram plus working code | Architecture diagram plus capacity numbers |
| Concurrency means | Locks, thread-safe collections, atomic updates in one process | Distributed locks, replication, consensus across machines |
| Typical tools | SOLID, design patterns, UML | CAP theorem, sharding, caching, message queues |
| Example prompt | Design the classes for a parking lot | Design a parking app for 10,000 lots nationwide |
| Who is usually asked | Freshers to senior engineers | Mostly 2+ years of experience, though freshers get basics |
The two overlap. A good LLD answer for a ticket-booking system still has to think about two users grabbing the same seat, and a good HLD answer still needs sensible data models. If you want the system-level side, the system design fundamentals lesson is the starting point.
Terminology
An entity is a thing in the problem domain that has identity and state, such as a User, Order or ParkingSpot. A service (in LLD) is a class that coordinates entities to perform a use case, such as BookingService.book(...). Keeping these two separate is one of the simplest ways to make a design readable.
The two interview formats
Format 1: whiteboard or discussion design (45–60 minutes)
You talk through the design with the interviewer, often on a shared document or a whiteboard tool. You list requirements, draw a class diagram, write the signatures of the key methods and perhaps code one or two core methods in full. The interviewer pushes on choices: "Why is this an interface?", "What if we add electric-vehicle charging spots?", "What happens when two cars arrive at the same time?"
What wins here is clear reasoning spoken aloud. A tidy diagram with good names and a sensible explanation of trade-offs beats a large diagram you cannot defend.
Format 2: machine coding round (60–120 minutes, commonly 90)
You receive a written problem statement, for example "Implement Splitwise with these five commands", and must write working code on your own machine, often in your preferred language. Typical rules:
- The code must run. Many companies give sample input and expected output, or a
mainmethod driver. - No database or framework is expected; in-memory storage (maps and lists) is normal.
- After coding, there is a 15–30 minute review. The interviewer reads your code, asks you to add a feature live, and checks how many files you had to touch.
- Some companies evaluate the submission offline afterwards.
What wins here is a working, well-structured core. A design that covers 80% of requirements and runs cleanly beats a design that covers 100% on paper and crashes.
| Discussion round | Machine coding round | |
|---|---|---|
| Duration | 45–60 min | 60–120 min |
| Output | Diagram plus key methods | Complete runnable program |
| Biggest risk | Vague hand-waving | Running out of time with broken code |
| Extension test | Verbal "what if" questions | Add a feature live during review |
| Scoring weight | Reasoning and trade-offs | Correctness, structure and completeness |
Interview tip
At the start, ask which format it is: "Would you like working code at the end, or should I focus on the class design and the key methods?" Interviewers appreciate it, and it tells you how to spend your time.
What interviewers evaluate
Interview rubrics differ between companies, but nearly all of them check the same eight areas. Knowing them lets you make each one visible on purpose.
1. Requirement gathering. Did you clarify scope before coding? A parking lot problem is very different if it has one floor or twenty, if it charges by the hour or a flat fee, if it supports reservations or not. Candidates who start coding immediately usually build the wrong thing.
2. Entity modelling. Did you find the right nouns and give them the right responsibilities? A common failure is a single ParkingLotManager class with 40 methods — often called a god class, meaning one class that knows and does everything.
3. Extensibility. Can a new requirement be added mostly by writing new code rather than editing old code? If adding a "truck" vehicle type means editing five if-else chains, the design is brittle. If it means adding one new class, it is extensible.
4. SOLID and clean code. SOLID is a set of five object-oriented design principles (Single responsibility, Open/closed, Liskov substitution, Interface segregation, Dependency inversion), covered in the SOLID principles lesson. Interviewers do not want you to recite them; they want to see them in your code: small classes, interfaces at the right places, dependencies passed in rather than created inside.
5. Design patterns, used appropriately. A design pattern is a named, reusable solution to a recurring design problem, such as Strategy or Observer. Using Strategy for pricing rules is a plus. Forcing five patterns into a 200-line program is a minus. You should be able to say why you used each one.
6. Concurrency. Many LLD problems involve shared resources: a seat, a parking spot, a wallet balance. Interviewers ask what happens when two threads book the same seat. You should know at least synchronized, locks, ConcurrentHashMap and atomic classes, covered in the synchronization lesson from the operating-systems track.
7. Testability. Can each class be tested in isolation? If your PaymentService creates its own new RazorpayClient() inside a method, you cannot test it without a real network. If the client is passed in through the constructor, you can pass a fake.
8. Working code. In machine coding rounds this is the gate: non-running code often means rejection regardless of design quality. Even in discussion rounds, writing two core methods correctly shows you can actually implement the design.
Common misconception
"More patterns means a better score." It does not. Interviewers mark down unnecessary abstraction — for example an AbstractVehicleFactoryProvider for three vehicle types that never change. Every class and interface should solve a problem you can name.
A repeatable framework
The biggest difference between strong and weak candidates is not knowledge of patterns. It is having a process, so that nothing important is forgotten under pressure. Here is an eleven-step framework. You will reuse it in every case study in this track, from the parking lot to the vending machine.
1 Clarify requirements 7 Choose patterns
2 List use cases 8 Handle concurrency
3 Identify entities 9 Write the code
4 Map relationships 10 Test with a driver/main
5 Design classes 11 Discuss extensions
6 Define key methods/APIs
Step 1: Clarify requirements (5 minutes)
Split requirements into functional (what the system does: "a user can book a seat") and non-functional (qualities: "two users must never get the same seat", "must be easy to add new payment methods"). Ask questions that change the design, not questions that only change constants.
Good questions change the class structure: "Can a booking contain multiple seats?", "Do we need to support cancellation and refunds?", "Is pricing different for weekends?" Weak questions do not: "What is the maximum seat count?" (it is just a number).
End this step by writing down the agreed list and explicitly marking things out of scope. Saying "I will not handle payments in depth; I will model them behind an interface" is a strong move.
Step 2: List use cases (3 minutes)
A use case is one interaction an actor (a user or another system) has with the system, written as a verb phrase: "park vehicle", "unpark and pay", "view free spots". Use cases become your public methods later, so this list is effectively your API.
Step 3: Identify entities (5 minutes)
Underline the nouns in the requirements. Each noun is a candidate class. Then filter:
- Keep nouns with state and behaviour of their own (
Tickethas an entry time and computes duration). - Turn nouns that are just a fixed set of values into enums (
VehicleType.CAR,SpotSize.LARGE). - Turn nouns that are just a number or string into fields (a "licence plate" is a
StringonVehicle, not a class, unless it needs validation logic). - Drop nouns that describe the whole problem ("system", "application").
Step 4: Map relationships (3 minutes)
For each pair of entities, ask: does one contain the other (a Floor has Spots), use the other (a PricingStrategy uses a Ticket), or is a kind of the other (a Car is a Vehicle)? Also decide multiplicity, meaning how many of one relate to how many of the other: one floor has many spots; one ticket refers to exactly one spot. The UML and class diagrams lesson covers the notation.
Step 5: Design classes (5 minutes)
For each class write its fields and its responsibility in one sentence. If you cannot describe a class in one sentence without using "and" twice, it probably does too much. Decide which things vary and should sit behind interfaces: pricing rules, notification channels, payment methods and spot-allocation rules are the usual suspects.
Step 6: Define key methods and APIs (5 minutes)
Write the signatures of public methods, including inputs, outputs and failure behaviour. For example: Ticket park(Vehicle v) throws NoSpotAvailableException when full. Agreeing on signatures with the interviewer before writing bodies prevents large rewrites.
Step 7: Choose patterns (2 minutes)
Look at what varies and pick a pattern only if it fits. Common pairings in LLD problems:
| What varies | Pattern | Example |
|---|---|---|
| An algorithm or rule | Strategy | Pricing, spot allocation, split type in Splitwise |
| Behaviour depending on current mode | State | Vending machine, elevator, order lifecycle |
| Reacting to events | Observer | Notify users when a seat frees up |
| Creating one of many types | Factory | Create the right vehicle or payment object |
| Building complex objects | Builder | Build an Order with many optional fields |
Step 8: Handle concurrency (3 minutes)
Name the shared mutable state (a spot's occupied flag, a seat's status, a wallet balance) and say how you protect it: a lock per resource, an atomic compare-and-set, or a ConcurrentHashMap. Even if you do not code it fully, saying "the critical section is assign-spot; I will lock per floor to avoid a global bottleneck" shows awareness.
Step 9: Write the code (40–50 minutes in machine coding)
Write in this order: enums and simple value classes, then entities, then interfaces and strategies, then services, then the driver. Get one use case working end to end before adding others. This guarantees you always have something that runs.
Step 10: Test with a driver (5 minutes)
Write a main method that runs the main scenarios and prints results, including at least one failure case (lot full, insufficient balance). If time allows, a couple of unit tests for the trickiest logic, such as fee calculation.
Step 11: Discuss extensions (5 minutes)
Proactively say how the design handles likely changes: "To add EV charging, I add a SpotType.EV and a new allocation strategy; no existing class changes." This is where SOLID and patterns pay off visibly.
The time budget
Here is a budget for a 90-minute machine coding round. For a 45-minute discussion round, halve steps 1–8 and replace step 9 with writing two or three core methods.
| Phase | Steps | Minutes | Running total |
|---|---|---|---|
| Understand | 1–2 | 8 | 8 |
| Model | 3–6 | 18 | 26 |
| Decide | 7–8 | 5 | 31 |
| Build | 9 | 45 | 76 |
| Verify | 10 | 6 | 82 |
| Discuss | 11 | 8 | 90 |
Interview tip
Say the framework out loud at the start: "I'll spend a few minutes on requirements, then sketch the entities and their relationships, agree the main APIs with you, and then code." It shows structure and gives the interviewer a chance to redirect you early.
Worked mini-example: a logger library
Let us apply the full framework to a small but realistic problem. It is small enough to finish here, yet it touches entities, interfaces, patterns, concurrency and extension.
Problem. Design a logging library. Application code calls it to log messages at different levels. Messages should go to one or more destinations. It should be easy to add new destinations.
Step 1: Clarify
Questions you might ask, with plausible answers from the interviewer:
- Which levels? —
DEBUG,INFO,WARN,ERROR. - Can the minimum level be configured, so
DEBUGis hidden in production? — Yes. - Which destinations? — Console and file for now; more later (for example a remote server).
- Can one message go to several destinations at once? — Yes.
- Format of a line? — Timestamp, level, message.
- Will many threads log at the same time? — Yes.
- Asynchronous logging, log rotation? — Out of scope; mention as extensions.
Requirements after clarifying:
- Functional: log at four levels; filter below a configured minimum; write to one or more destinations; format as
timestamp [LEVEL] message. - Non-functional: thread-safe; easy to add destinations and formats; easy to test.
Step 2: Use cases
- Application logs a message at a level.
- Administrator configures the minimum level.
- Administrator adds a destination.
Step 3: Entities
Nouns from the requirements: logger, message, level, destination, format, timestamp.
LogLevel— a fixed set of values with an ordering, so an enum.LogMessage— holds level, text and timestamp. It never changes after creation, so an immutable value object (an object defined only by its values, with no identity of its own).Logger— the entry point application code calls.LogAppender— a destination. In logging libraries the conventional word for a destination is appender. Several kinds exist, so an interface withConsoleAppenderandFileAppenderimplementations.LogFormatter— turns a message into a line of text. Making it separate lets you change format without touching appenders.
Step 4: Relationships
+---------+ 1 * +---------------+
| Logger |--------->| <<interface>> |
+---------+ | LogAppender |
| +-------^-------+
| creates | implements
v +-------+--------+
+------------+ | |
| LogMessage | +-----------+ +--------------+
+------------+ | Console | | File |
| | Appender | | Appender |
v +-----------+ +--------------+
+----------+ | each appender has one
| LogLevel | v
+----------+ +---------------+
| LogFormatter |
+---------------+
A Logger has many appenders (one-to-many). Each appender uses one formatter. A LogMessage has one LogLevel.
Steps 5 and 6: Classes and APIs
Logger:debug(String),info(String),warn(String),error(String),setMinLevel(LogLevel),addAppender(LogAppender).LogAppender:void append(LogMessage m).LogFormatter:String format(LogMessage m).
Step 7: Patterns
- Strategy:
LogFormatteris a strategy, because the formatting algorithm is swappable. - Observer-like fan-out: the logger forwards every message to all registered appenders, much like a subject notifying observers.
- Not a Singleton. Many tutorials make the logger a singleton, but that makes testing harder because every test shares the same global instance. We pass the logger in where it is needed. This choice is worth saying aloud; the creational patterns lesson explains it in detail.
Step 8: Concurrency
Shared mutable state: the list of appenders, the minimum level, and the file handle in FileAppender. Our choices:
- Appenders list:
CopyOnWriteArrayList, a thread-safe list that copies its array on every write. It suits us because appenders are added rarely and read on every log call. - Minimum level:
volatile, so a change by one thread is visible to all threads immediately. - Each appender protects its own output with
synchronized, so two lines never interleave in the middle.
Step 9: Code
import java.time.Instant;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
enum LogLevel {
DEBUG, INFO, WARN, ERROR;
boolean isAtLeast(LogLevel other) {
return this.ordinal() >= other.ordinal();
}
}
final class LogMessage {
private final LogLevel level;
private final String text;
private final Instant timestamp;
LogMessage(LogLevel level, String text, Instant timestamp) {
this.level = level;
this.text = text;
this.timestamp = timestamp;
}
LogLevel level() { return level; }
String text() { return text; }
Instant timestamp() { return timestamp; }
}
interface LogFormatter {
String format(LogMessage m);
}
class SimpleFormatter implements LogFormatter {
public String format(LogMessage m) {
return m.timestamp() + " [" + m.level() + "] " + m.text();
}
}
interface LogAppender {
void append(LogMessage m);
}
class ConsoleAppender implements LogAppender {
private final LogFormatter formatter;
ConsoleAppender(LogFormatter formatter) { this.formatter = formatter; }
public synchronized void append(LogMessage m) {
System.out.println(formatter.format(m));
}
}
// Collects lines in memory; handy in tests instead of a real file.
class InMemoryAppender implements LogAppender {
private final LogFormatter formatter;
private final List<String> lines = new CopyOnWriteArrayList<>();
InMemoryAppender(LogFormatter formatter) { this.formatter = formatter; }
public void append(LogMessage m) { lines.add(formatter.format(m)); }
List<String> lines() { return List.copyOf(lines); }
}
class Logger {
private final List<LogAppender> appenders = new CopyOnWriteArrayList<>();
private volatile LogLevel minLevel = LogLevel.INFO;
void addAppender(LogAppender a) { appenders.add(a); }
void setMinLevel(LogLevel level) { this.minLevel = level; }
void debug(String text) { log(LogLevel.DEBUG, text); }
void info(String text) { log(LogLevel.INFO, text); }
void warn(String text) { log(LogLevel.WARN, text); }
void error(String text) { log(LogLevel.ERROR, text); }
private void log(LogLevel level, String text) {
if (!level.isAtLeast(minLevel)) {
return;
}
LogMessage m = new LogMessage(level, text, Instant.now());
for (LogAppender a : appenders) {
a.append(m);
}
}
}
class LoggerDemo {
public static void main(String[] args) {
Logger logger = new Logger();
InMemoryAppender memory = new InMemoryAppender(new SimpleFormatter());
logger.addAppender(new ConsoleAppender(new SimpleFormatter()));
logger.addAppender(memory);
logger.debug("hidden: below INFO");
logger.info("server started");
logger.error("disk almost full");
System.out.println("captured lines = " + memory.lines().size());
}
}
Running LoggerDemo prints two log lines (the DEBUG one is filtered out) followed by captured lines = 2.
Notice the order we wrote things: enum, value object, interfaces, implementations, the coordinating Logger, and finally the driver. Every piece compiled and ran before the next was added.
Step 10: Test
The InMemoryAppender doubles as a test tool. A unit test can create a Logger, add an InMemoryAppender, log three messages at different levels, and assert exactly which lines were captured. No files or console capture is needed. That is testability coming directly from the decision to make appenders an interface and pass them in.
Step 11: Extensions
| New requirement | Change needed | Existing classes edited |
|---|---|---|
| Send logs to a remote server | New HttpAppender implements LogAppender | None |
| JSON output | New JsonFormatter implements LogFormatter | None |
| Per-appender minimum level | Add a level field to an appender wrapper | None (wrap with a filtering appender) |
| Asynchronous logging | AsyncAppender that queues messages and writes on a background thread | None |
| Log rotation by file size | Logic inside FileAppender only | One |
This table is exactly what you want to show an interviewer: most changes are additions, not edits. That property is the open/closed principle in action.
The same idea in Python
Python has no interface keyword, but abstract base classes (abc.ABC) give the same contract. The structure is identical:
from abc import ABC, abstractmethod
from datetime import datetime
from enum import IntEnum
class LogLevel(IntEnum):
DEBUG = 10
INFO = 20
WARN = 30
ERROR = 40
class Appender(ABC):
@abstractmethod
def append(self, level: LogLevel, text: str) -> None: ...
class MemoryAppender(Appender):
def __init__(self):
self.lines = []
def append(self, level, text):
self.lines.append(f"{datetime.now():%H:%M:%S} [{level.name}] {text}")
class Logger:
def __init__(self, min_level=LogLevel.INFO):
self.min_level = min_level
self.appenders = []
def log(self, level, text):
if level < self.min_level:
return
for a in self.appenders:
a.append(level, text)
mem = MemoryAppender()
log = Logger()
log.appenders.append(mem)
log.log(LogLevel.DEBUG, "hidden")
log.log(LogLevel.WARN, "low memory")
print(len(mem.lines)) # 1
How to talk while designing
Interviewers can only score what they hear or see. Narrate decisions, not keystrokes:
- "I am making
PricingStrategyan interface because the problem says weekend pricing may change; a new rule becomes a new class." - "I am keeping
Ticketimmutable except for its exit time, so it is safe to share between threads." - "I am using an in-memory
HashMaprepository. In production this would sit behind the same interface but talk to a database."
When the interviewer suggests a change, do not get defensive. Say what it would touch: "That would mean a new state in the enum and one new transition in the state class; no other class changes."
Common mistakes
- Coding before clarifying. You build multi-floor support nobody asked for and miss the reservation feature they did ask for.
- The god class. One
Systemclass with every method. Split by responsibility: entities hold their own data and simple rules; services coordinate. - Anaemic everything. The opposite extreme: entities with only getters and setters, and all logic in services. A
Ticketcan compute its own duration; let it. if-elseon type everywhere.if (vehicle.type == CAR) ... else if (BIKE) ...scattered across classes. Use polymorphism (a method on each type) or an enum with behaviour.- Pattern stuffing. Adding Factory, Builder, Singleton and Visitor because you memorised them. Every pattern should answer "what changes?"
- Ignoring concurrency. Never mentioning what happens when two users act at once on the same seat, spot or account.
- Public mutable fields.
public List<Spot> spotslets any caller corrupt the state. Keep fields private; expose behaviour. - Running out of time with nothing runnable. Build one use case end to end first, then widen.
- Hard-coding dependencies.
new SmsService()insideBookingServicemakes testing and swapping impossible. Pass dependencies through the constructor. - Silent failures. Returning
nullwhen the lot is full. Prefer a clear exception or anOptional, and say which you chose.
Common mistake
Spending 30 minutes on a beautiful diagram and 15 minutes on code in a machine coding round. The diagram is a means; the reviewer runs your code. Keep the diagram rough and move to code by about the 30-minute mark.
How this track is organised
The track has two halves. Lessons 2–7 give you the toolkit: OOP for LLD, SOLID, UML, and the creational, structural and behavioural patterns. Lessons 8–13 apply the framework to the most commonly asked problems: parking lot, elevator, movie ticket booking, Splitwise, LRU cache and rate limiter, and vending machine and ATM. The final lesson, LLD interview practice, is a question bank.
Interview questions
Q1. What is the difference between low-level design and high-level design?
HLD decides the components of a system — services, databases, caches, queues — and how they communicate to meet scale and availability goals. LLD decides the code inside one component: classes, interfaces, methods and their relationships, aiming for readability, extensibility and correctness. HLD worries about network partitions; LLD worries about a class doing too much or two threads updating the same object.
Q2. How do you start an LLD interview?
Clarify requirements first, separating functional from non-functional ones and stating what is out of scope. Then list use cases, which become the public API. Only after agreeing scope do I identify entities and draw relationships. I also ask whether the interviewer wants runnable code or a design discussion.
Q3. How do you identify classes from a problem statement?
I underline nouns and filter them. Nouns with their own state and behaviour become classes, fixed sets of values become enums, simple attributes become fields, and words describing the whole system are dropped. Then I check each class has one clear responsibility I can state in a sentence.
Q4. What makes a design extensible?
New requirements can be met mostly by adding code rather than modifying existing code. In practice that means putting things likely to change behind interfaces, using polymorphism instead of type checks, and injecting dependencies. I demonstrate it by walking through a likely change and naming exactly which files change.
Q5. What is a god class and how do you avoid it?
A god class knows and does too much — typically a single manager with dozens of methods and fields. It is hard to test and every change risks breaking something unrelated. I avoid it by giving entities their own behaviour, extracting variable algorithms into strategies, and keeping services thin coordinators of a single use case each.
Q6. When should you use a design pattern?
When there is a concrete problem the pattern solves: an algorithm that varies (Strategy), behaviour depending on state (State), objects reacting to events (Observer). I name the problem first, then the pattern. Using a pattern without a problem adds indirection and is marked down.
Q7. How do you handle concurrency in an LLD problem?
I identify shared mutable state, such as a seat's status, and the critical sections that read and update it. Then I choose the narrowest protection that is correct: a per-resource lock, an atomic compare-and-set, or a concurrent collection. I avoid one global lock because it serialises everything, and I mention deadlock risk if multiple locks are taken.
Q8. Why should dependencies be injected rather than created inside a class?
If a class creates its own collaborator with new, it is tied to that exact implementation and cannot be tested without it. Passing the dependency through the constructor lets tests pass a fake and lets production swap implementations without editing the class. This is the dependency-inversion principle in practical form.
Q9. In a machine coding round, how do you manage time?
Roughly 25–30 minutes for requirements and modelling, 45 minutes coding, and the rest for a driver, testing and discussion. I code one use case end to end first so I always have runnable code, then add the rest. I keep storage in memory behind a repository interface rather than spending time on persistence.
Q10. What should you do when the interviewer adds a new requirement midway?
Restate it, then explain which parts of the design it touches before writing code. If the design is good, the change is a new class or a new enum value plus a small wiring change. If it needs larger edits, say so honestly and explain how you would refactor so the next such change is cheap.
Q11. Should a logger be a Singleton?
It often is in tutorials, but a global singleton makes tests share state and hides the dependency. A better approach is to create one logger at application start-up and pass it to classes that need it, or obtain it from a factory keyed by class name as real logging libraries do. You keep one instance per name without a hard-coded global.
Q12. How do you test an LLD solution quickly?
Write a driver main that runs each use case, including failure cases such as "lot full". Design for testability by putting external effects (console, files, payments, time) behind interfaces so tests can use in-memory fakes. If time allows, add unit tests for tricky rules like fee calculation.
Q13. What are functional and non-functional requirements in LLD?
Functional requirements describe behaviour: "a user can cancel a booking". Non-functional requirements describe qualities: thread safety, extensibility, performance of a lookup, testability. In LLD, non-functional requirements mainly drive structural choices such as interfaces, locking and data-structure selection.
Key takeaways
- LLD designs the classes inside one component; HLD designs the components and their connections.
- Expect either a discussion round (diagram plus key methods) or a machine coding round (runnable code in 60–120 minutes).
- Interviewers score requirements, modelling, extensibility, SOLID, sensible patterns, concurrency, testability and working code.
- Use the framework: clarify, use cases, entities, relationships, classes, APIs, patterns, concurrency, code, test, extensions.
- Put what varies behind interfaces and inject dependencies; most new requirements should become new classes.
- Name a concrete problem before naming a pattern.
- Always identify shared mutable state and say how you protect it.
- In machine coding, get one use case running end to end before widening.
Next lesson
Continue with OOP for low-level design.

