Prepare for LLD interview questions grouped by experience level.
LLD Interview Question & Answers
0-2 Years
Low level design is the process of defining the internal structure of a system at the class and object level, including class attributes, methods, relationships, and how components interact. It sits between high level architecture and actual code implementation.
High level design defines the overall system architecture, like which services exist and how they communicate, while low level design defines the internal structure of each component, like class diagrams and method signatures within a single service.
A class diagram is a UML diagram that shows the classes in a system, their attributes, methods, and the relationships between them, such as inheritance, association, and composition. It's the primary tool for communicating low level design.
The four pillars are encapsulation, abstraction, inheritance, and polymorphism. Encapsulation bundles data and methods together, abstraction hides implementation detail, inheritance lets classes share behavior, and polymorphism lets objects of different types respond to the same method call differently.
SOLID is an acronym for five design principles: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. Together they guide how to structure classes so a codebase stays maintainable as it grows.
The Single Responsibility Principle states that a class should have only one reason to change, meaning it should be responsible for a single, well-defined piece of functionality rather than mixing unrelated concerns.
The Open-Closed Principle states that a class should be open for extension but closed for modification, meaning new behavior should be added by extending existing code rather than editing it directly.
The Liskov Substitution Principle states that objects of a subclass should be replaceable with objects of their parent class without breaking the program's correctness, meaning subclasses must honor the behavior contracts of their parent.
The Interface Segregation Principle states that clients shouldn't be forced to depend on methods they don't use, meaning large interfaces should be split into smaller, more specific ones.
The Dependency Inversion Principle states that high level modules shouldn't depend directly on low level modules, both should depend on abstractions, so implementation details can change without affecting the higher level logic.
A design pattern is a reusable, proven solution to a commonly occurring software design problem, providing a shared vocabulary and template that developers can adapt to their specific context rather than solving the same problem from scratch each time.
The three categories are creational patterns, which deal with object creation, structural patterns, which deal with how classes and objects are composed, and behavioral patterns, which deal with how objects communicate and assign responsibility.
The Singleton pattern ensures a class has only one instance and provides a global point of access to it, commonly used for things like a shared configuration manager or a connection pool.
The Factory pattern defines an interface for creating an object but lets subclasses decide which class to instantiate, decoupling the code that uses an object from the code that creates it.
The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different variations, often used when an object has many optional parameters.
The Observer pattern defines a one-to-many dependency between objects so that when one object, the subject, changes state, all its dependents, the observers, get notified automatically. It's commonly used for event-driven systems.
The Strategy pattern defines a family of interchangeable algorithms, encapsulates each one, and lets the client choose which to use at runtime, avoiding large conditional blocks that pick behavior based on type.
The Decorator pattern lets you add new behavior to an object dynamically by wrapping it in a decorator object, without modifying the original class or affecting other instances of the same class.
The Adapter pattern converts the interface of one class into another interface that a client expects, allowing classes with incompatible interfaces to work together without modifying either one.
Composition builds functionality by combining objects that contain other objects, while inheritance builds functionality by having a class extend a parent class. Composition is often preferred because it avoids the tight coupling and fragile hierarchies that deep inheritance can create.
An abstract class is a class that cannot be instantiated on its own and typically contains at least one abstract method that subclasses must implement, serving as a partial template for related classes.
An interface defines a contract of methods that a class must implement, without providing any implementation itself, allowing different classes to be used interchangeably as long as they fulfill the same contract.
Association represents a general relationship between two classes where one uses or interacts with the other, without one being a structural part of the other, such as a Teacher class associated with a Student class.
Aggregation is a special form of association representing a whole-part relationship where the part can exist independently of the whole, such as a Department containing Employees who could exist without that department.
Composition is a stronger form of aggregation where the part cannot exist independently of the whole, such as a House containing Rooms, since a Room typically doesn't make sense without its House.
Method overloading is defining multiple methods with the same name but different parameter lists within the same class, allowing the method to behave differently depending on the arguments passed.
Method overriding is when a subclass provides its own implementation of a method already defined in its parent class, allowing the subclass to change or extend the inherited behavior.
Coupling refers to how dependent one class or module is on another. Low coupling means classes can change independently without breaking each other, which is generally a design goal.
Cohesion refers to how closely related the responsibilities of a single class or module are. High cohesion means a class does one clearly defined thing well, which usually leads to more maintainable code.
A sequence diagram shows how objects interact over time by depicting the order of messages exchanged between them for a specific scenario or use case, useful for visualizing runtime behavior.
An abstract class can contain both implemented and unimplemented methods and supports single inheritance, while an interface traditionally contains only method signatures and a class can implement multiple interfaces.
LLD interviews assess a candidate's ability to translate a real-world problem, like designing a parking lot or a library system, into well-structured classes with appropriate relationships and design patterns, reflecting how they'd actually structure production code.
A use case diagram shows the interactions between actors, like users or external systems, and the system's functionality, providing a high level view of what the system does before diving into class-level detail.
Polymorphism lets a single method call behave differently depending on the object it's called on. For example, calling a draw method on a list of Shape objects would render a circle differently than a square, even though both respond to the same method name.
Encapsulation means bundling an object's data with the methods that operate on it and restricting direct access to that data from outside the class, typically using private fields and public getter or setter methods.
An is-a relationship represents inheritance, like a Car is-a Vehicle, while a has-a relationship represents composition or aggregation, like a Car has-a Engine. Choosing the right one avoids inappropriate inheritance hierarchies.
3-6 Years
I would start by identifying the core entities, like ParkingLot, ParkingSpot, Vehicle, and Ticket, then define relationships between them, such as a ParkingLot containing multiple ParkingSpots. I would use a Strategy pattern for spot allocation so different allocation rules, like nearest spot first, can be swapped without changing the core logic.
I would model Book, Member, and Loan as core entities, with a Catalog class managing book search and availability. I would use the Observer pattern to notify members when a reserved book becomes available, and keep fine calculation logic in its own class following single responsibility.
I would default to composition unless there's a clear is-a relationship that holds true in every case, since composition avoids the fragile base class problem where changes to a parent unexpectedly break subclasses. I'd only reach for inheritance when the shared behavior is genuinely structural, beyond just convenient.
I would use a locking mechanism at the seat level so two users can't book the same seat simultaneously, combined with a Strategy pattern for different pricing rules, like weekday versus weekend pricing. I would keep the seat locking logic isolated so it can be swapped for a distributed lock if the system later scales across multiple servers.
I would model states like Idle, HasMoney, Dispensing, and OutOfStock as separate classes implementing a common State interface, with the vending machine delegating behavior to its current state object. This avoids a large conditional block checking the current state everywhere in the code.
I would define a common NotificationSender interface with implementations for each channel, then use a Factory to instantiate the right sender based on user preference. This follows the Open-Closed Principle since adding a new channel means adding a new class rather than modifying existing dispatch logic.
I would model each piece type, like Pawn and Knight, as a subclass of an abstract Piece class with its own move validation logic, following polymorphism so the Board class doesn't need type-checking conditionals for each piece. I would keep the Board and Game classes separate so board state and game flow, like turn order and check detection, stay independent responsibilities.
I would define a RateLimiter interface with different implementations, like token bucket and sliding window, following the Strategy pattern so the algorithm can be swapped based on the use case. I would keep the storage of request counts abstracted behind an interface too, so it can back onto in-memory storage or something external later.
I would model Elevator, ElevatorController, and Request as core classes, with the controller responsible for deciding which elevator handles an incoming request using a pluggable scheduling strategy. Keeping the scheduling algorithm behind a Strategy interface lets you swap in more sophisticated logic later without touching the elevator or controller classes.
I would keep Order, Payment, and Delivery as separate classes with clear boundaries, using the Observer pattern so a payment confirmation event triggers delivery assignment rather than tightly coupling the payment class to delivery logic directly.
I would model a base Beverage class and wrap it with decorators like Milk or Sugar that each add cost and description, so any combination of add-ons can be composed without creating a combinatorial explosion of subclasses for every possible combination.
I would define a LogAppender interface with implementations for console, file, and remote logging, letting a Logger class hold a list of appenders it writes to, so adding a new destination doesn't require touching the core Logger class.
I would model User, Expense, and Balance as core entities, with a Strategy pattern for split types, like equal split versus percentage split, so new splitting rules can be added without modifying the core Expense class.
I would keep Inventory tracking per warehouse in its own class, with a coordinating service aggregating totals across warehouses, so a stock update in one location doesn't require locking or touching data for other warehouses.
I look for a class that has multiple unrelated reasons to change, such as a class handling both business logic and data persistence, since those two concerns tend to evolve independently and coupling them makes the class harder to test and modify safely.
I would define a Cache interface with a pluggable eviction strategy, like LRU or LFU, implemented as separate Strategy classes, so the eviction algorithm can change without touching the code that reads and writes cache entries.
I would separate Room, RoomType, and Reservation as distinct classes, with availability checking logic living in its own service rather than inside the Room class, keeping the Room class focused only on representing a room's static attributes.
I would define a PaymentMethod interface with implementations for credit card, wallet, and bank transfer, used through a Factory or Strategy pattern at checkout, so adding a new payment method later doesn't require modifying the checkout flow itself.
I would model the StockPrice as the subject and individual Alert instances as observers, so when the price updates, every registered alert gets checked without the price update logic needing to know anything about how alerts are evaluated.
I would use the Command pattern, where each user action is encapsulated as a Command object with an execute and undo method, stored in a stack, so undo and redo just involve popping and replaying commands rather than manually tracking state diffs.
I would model Board, Player, and Game as separate classes, with the Game class orchestrating turn order and win-condition checking, keeping the Board class responsible only for tracking cell state rather than game rules.
I would use the Chain of Responsibility pattern for discount rules, where each discount handler checks if it applies and either processes it or passes the cart to the next handler, so new discount types can be added as new handlers without modifying existing ones.
I would look for opportunities to replace some of the inheritance with composition, particularly where subclasses only differ in a small piece of behavior, since deep hierarchies tend to make it hard to reason about which level a given behavior actually comes from.
I would model Account, Card, Transaction, and CashDispenser as separate classes, using the State pattern for the ATM's operational states like idle, card inserted, and dispensing, so transitions between states stay explicit rather than buried in conditionals.
6-8 Years
I would define stable extension interfaces following the Open-Closed and Dependency Inversion principles, then use a registry pattern where plugins register themselves at startup. I would version the interface contract carefully since breaking it later would force every third-party plugin to update simultaneously.
I would check whether each pattern is solving a real variability requirement that's actually expected to change, since applying a Strategy or Factory pattern where there's only ever going to be one implementation just adds indirection without benefit. Patterns should follow a genuine need, not be added preemptively.
I would define a CacheClient interface abstracting the backend, with implementations for different providers behind a Factory, and keep serialization logic as a separate pluggable component so changing the backend doesn't force changing how objects are serialized.
I would look for the common abstraction both hierarchies are really modeling and propose extracting a shared interface or base class, while being careful not to force a merge that creates awkward coupling if the two teams' actual requirements genuinely diverge in ways that matter.
I would model each rule as a data-driven object implementing a common Rule interface, with a RuleEngine that evaluates rules in a defined order, keeping the evaluation logic generic enough that new rule types can be added by implementing the interface rather than modifying the engine.
I would look for classes with hidden dependencies, like instantiating collaborators directly inside methods rather than receiving them through constructor injection, since that tight coupling to concrete implementations is usually the root cause of tests that need excessive mocking or can't isolate the unit under test.
I would separate the storage structure, the expiration policy, and the eviction strategy into distinct classes so each can be tested and swapped independently, using a background thread or lazy expiration check depending on the performance tradeoffs the use case demands.
I would define a common FileSystem interface with methods like read, write, and list, then implement it separately for local disk and cloud storage, making sure the interface doesn't leak implementation details, like cloud-specific error codes, that would break the local implementation's contract.
I would check whether a subclass strengthens preconditions or weakens postconditions in a way that would break callers expecting the parent class's contract, such as a subclass throwing an exception the parent never throws for the same input, since that kind of violation often passes compilation but breaks at runtime.
I would use the Composite pattern, where a CompositeValidator holds a list of individual validators and aggregates their results, letting callers build up complex validation logic from simple, independently testable rule objects.
I would identify distinct clusters of related methods and fields within the class, since those clusters usually reveal the separate responsibilities that should become their own classes, then extract them incrementally while keeping existing tests passing at each step.
The Visitor pattern lets you add new operations across a hierarchy without modifying the classes themselves, which is useful when operations change more often than the class structure, but it adds real complexity and works against you if the class hierarchy itself is what changes frequently.
8-10 Years
I would require documentation for patterns that aren't obvious from reading the code alone, like a non-standard variation of a well-known pattern, while trusting that clearly named, conventionally implemented patterns are self-explanatory to any engineer familiar with them. Over-documenting obvious patterns just adds noise.
I would scale the review depth to the component's expected lifespan and blast radius, requiring a formal class diagram review for core shared infrastructure while letting isolated, low-risk features move faster with lighter review. Uniform heavy process for every change slows teams without proportional benefit.
I would sample class structures across a few different teams' codebases and check whether a developer moving between teams would recognize familiar patterns, since inconsistent conventions increase onboarding time and make code review harder for anyone outside the original team.
I would track which design decisions are showing strain, like a Singleton that's now a bottleneck under higher concurrency, and prioritize refactoring based on how much future development that debt is actively slowing down, rather than refactoring reactively only after an incident.
I would require interfaces to be reviewed with an eye toward what's genuinely likely to change versus what's an implementation detail leaking through, since a poorly abstracted interface forces breaking changes on every consumer whenever the implementation needs to evolve.
I would gather concrete examples of teams working around the library rather than using it as intended, since that's the clearest signal the abstraction no longer fits real usage patterns, and weigh a redesign against the migration cost for every team currently depending on it.
I would require a proposal showing the pattern solves a problem the team has hit more than once, rather than approving patterns speculatively, since patterns adopted without a proven recurring need tend to get misapplied by teams that copy them without understanding the underlying tradeoff.
I would weigh the flexibility benefit against the friction it creates for engineers moving between teams or reviewing cross-team code, and typically land on a shared baseline for foundational concerns, like error handling and dependency injection style, while leaving room for team-specific choices elsewhere.
I would look at whether code review comments are repeatedly flagging the same class-design issues, like violated single responsibility or inappropriate inheritance, since a recurring pattern in review feedback signals a knowledge gap that individual reviews can't fix cost-effectively over time.
I would map how many of the planned features touch the same currently entangled classes, since a redesign is usually justified when several upcoming initiatives would otherwise each pay the cost of working around the same structural problem independently.
I would use concrete examples from the organization's own codebase, both where inheritance caused real pain and where composition worked cleanly, since abstract guidance lands less effectively than lessons drawn from problems the teams themselves have already lived through.
I would ask what specific, reasonably likely future requirement the extensibility is meant to support, and reject speculative flexibility built for needs nobody has actually articulated, since over-built abstractions often guess wrong about what actually needs to change.
I would require a brief rationale captured alongside significant design decisions, focused on why an approach was chosen over alternatives, since the code itself shows what was built but rarely explains why, and that context is what future engineers actually need when deciding whether to change it.
I would weigh the onboarding and cross-team collaboration cost of inconsistency against the genuine benefit of letting teams pick tools suited to their specific domain, generally leaning toward standardizing on one approach for the core testing and wiring benefits it brings organization-wide.
10+ Years
I have them practice explaining what problem a pattern solves before introducing its name, since engineers who learn patterns as vocabulary first tend to force-fit them into situations that don't need the added structure. I also review their designs by asking what would break if a requirement changed, which surfaces whether the pattern choice was actually load-bearing.
I would start by cataloging where design inconsistency has actually caused real friction, like onboarding delays or repeated review pushback, rather than mandating a fully worked-out style guide from scratch. I would staff it with senior engineers who've lived through the pain points they're now setting standards to prevent.
I frame it in terms of future velocity rather than present-day correctness, showing how a poorly structured class hierarchy has previously slowed a specific feature delivery, since leadership responds better to a concrete velocity cost than an abstract code quality argument.
I would move from ad hoc individual review toward documented, searchable design decisions and lightweight review checklists for common patterns, since informal tribal knowledge that worked at a smaller scale breaks down once no single reviewer has visibility into every team's codebase.
I try to ground the disagreement in concrete future requirements rather than abstract design philosophy, since two experienced engineers often agree once they're evaluating the same specific scenario rather than defending general principles. If it still doesn't resolve, I document the tradeoff explicitly and make the call rather than letting it stall.
I would require design decisions for significant components to be documented with their rationale, beyond just their final structure, and rotate ownership of core systems periodically so more than one person develops deep familiarity with each one.
I raise the precedent concern explicitly and explain what pattern of future requests it's likely to invite, letting the team weigh that against the immediate need, rather than blocking it outright if the immediate need is genuinely time sensitive.
I would identify engineers who already show strong judgment about design tradeoffs, beyond just technical breadth, and deliberately give them ownership of cross-team design decisions well before a transition, since that judgment under ambiguity is the hardest part of the role to develop quickly.
I focus on making sure the underlying design principles, like separation of concerns and dependency management, transfer across languages even when idiomatic syntax differs, since teams that treat a new framework as a reason to abandon established principles tend to relearn the same lessons the hard way.
I would present the specific initiatives currently blocked or slowed by the design debt, with a concrete cost estimate for continuing to work around it versus investing in a redesign, since executives need a decision framed in business impact rather than a technical description of the debt itself.
I would look at whether issues caught in design review are ones that would have been genuinely costly to fix later, versus reviewers nitpicking stylistic preferences, since a review process that mostly debates taste rather than catching real structural risk isn't earning its overhead.
I would quantify the recurring workaround cost teams are already paying to use the library as-is, since that ongoing tax, made visible in concrete terms, usually makes a stronger case than an abstract argument about code quality improving hypothetical future development.
I try to separate what's genuinely domain-specific from what's just unfamiliar, since teams sometimes resist shared conventions out of habit rather than real incompatibility. Where the domain difference is real, I'd rather carve out a documented exception than force a bad fit.
I would push the organization to invest more, not less, in clear architectural boundaries and interface contracts, since AI-generated code tends to amplify whatever structure already exists, good or bad, meaning a weak foundation gets weaker faster as more code gets generated against it.




