Low-Level Design Interview Guide: OOP, SOLID & Design Patterns with Examples
From Clean Object Modeling to GoF Design Patterns, Concurrency-Safe State Machines, and Machine Coding Mastery

What You Must Master to Clear This Track
- Never jump straight to writing code; spend the first 15 minutes defining entities, relationships, interfaces, and boundary contracts.
- Apply SOLID principles as practical guidelines to minimize ripple effects during requirement changes, not as dogmatic dogma.
- Master the big 8 design patterns that appear in 90% of interviews: Strategy, Factory, Observer, Decorator, Adapter, State, Singleton, and Composite.
- Always clarify concurrency requirements upfront: thread-safe data structures, synchronization scopes, and lock-free primitives prevent race conditions in machine coding rounds.
- Structure machine coding code with clean layered architecture: Model (Entities) -> Service/Manager -> Strategy/Policy -> Repository/Store -> Presentation/CLI.
Step-by-Step Study Plan
Follow this sequential roadmap designed to take you from core foundations to advanced architecture and mock interviews.
Core OOP & SOLID In-Depth
Master encapsulation, abstraction, inheritance vs composition, and the 5 SOLID principles with concrete violation and remediation examples.
- •Explain Open/Closed Principle (OCP) using Strategy or Factory patterns rather than nested if-else ladders.
- •Demonstrate Liskov Substitution Principle (LSP) violation using Rectangle-Square or ReadOnly-Writable collections.
- •Refactor tight coupling into Dependency Inversion (DIP) with dependency injection interfaces.
- •Code 5 classic refactoring katas identifying and fixing SOLID violations in Java, TypeScript, or Python.
- •Draw class diagrams and sequence diagrams to practice visual design modeling before typing code.
Gang of Four: Creational & Structural
Deep-dive into Factory Method, Abstract Factory, Builder, Singleton (thread-safe, enum, double-checked locking), Adapter, Decorator, Composite, and Facade.
- •Implement thread-safe Bill Pugh Singleton and Double-Checked Locking Singleton, explaining volatile memory barrier semantics.
- •Build a extensible Notification or Payment Gateway service using Factory + Adapter pattern.
- •Apply Decorator pattern for dynamically layering logging, compression, or encryption around streams.
- •Compare Adapter vs Decorator vs Facade vs Proxy in a comparative trade-off matrix.
- •Write clean unit tests verifying that adding a new concrete class does not touch existing factory consumer code.
Behavioral Patterns & Concurrency
Master Strategy, Observer, State, Command, Chain of Responsibility, and Template Method patterns combined with thread-safety mechanisms.
- •Implement Observer pattern with thread-safe subscriber registration and non-blocking asynchronous dispatch.
- •Model an Elevator or Vending Machine using the State pattern instead of hundreds of boolean state flags.
- •Use Chain of Responsibility for request validation, authentication, and rate limiting pipelines.
- •Implement an in-memory concurrent LRU/LFU cache with ReentrantReadWriteLock or ConcurrentHashMap + DoublyLinkedList.
- •Practice handling concurrent access to shared resources using optimistic and pessimistic locking.
90-Minute Machine Coding Simulation
Simulate end-to-end 90-minute live machine coding interviews for standard problems under time pressure with clean modular code and unit tests.
- •Complete Parking Lot system with multi-floor spot allocation, vehicle type strategies, and dynamic fee calculation.
- •Implement Splitwise expense-sharing system supporting equal, exact, and percentage splits with debt simplification.
- •Design Rate Limiter library supporting Token Bucket, Leaky Bucket, and Sliding Window Log algorithms.
- •Time yourself strictly: 15 min scoping, 25 min model & interfaces, 35 min implementation, 15 min edge cases & tests.
- •Ensure your solution compiles, executes cleanly via a CLI or main method, and handles invalid inputs gracefully.
SOLID Principles: Beyond Textbook Definitions
SOLID is not an abstract academic checklist; it is an engineering framework for making software resilient to inevitable business requirement changes without breaking existing functionality.
A class should have one, and only one, reason to change. Separate business rules, persistence logic, and presentation formatting into dedicated classes.
Software entities should be open for extension, but closed for modification. Introduce abstractions (interfaces/abstract classes) so new features require writing new classes rather than modifying existing tested code.
Subtypes must be substitutable for their base types without altering program correctness. Avoid overriding methods with no-ops, throwing UnsupportedOperationException, or tightening preconditions.
Clients should not be forced to depend on methods they do not use. Split fat, monolithic interfaces into fine-grained, role-specific interfaces.
High-level modules should not depend on low-level modules; both should depend on abstractions. Decouple core domain logic from specific databases, messaging queues, and third-party APIs.
High-level policy decoupling from low-level infrastructure via domain interfaces.
// VIOLATION: Adding a new payment method modifies existing class (violates OCP)
class PaymentProcessorBad {
process(type: string, amount: number) {
if (type === "CREDIT_CARD") { /* charge card */ }
else if (type === "PAYPAL") { /* charge paypal */ }
else if (type === "CRYPTO") { /* charge crypto */ }
else throw new Error("Unsupported payment type");
}
}
// REMEDIATION: Extensible Strategy Pattern adhering to OCP and DIP
interface PaymentStrategy {
pay(amount: number): Promise<PaymentReceipt>;
}
class CreditCardPayment implements PaymentStrategy {
constructor(private cardNumber: string, private cvv: string) {}
async pay(amount: number): Promise<PaymentReceipt> {
// Isolated credit card execution logic
return { status: "SUCCESS", transactionId: "CC-9821", amount };
}
}
class UPIPayment implements PaymentStrategy {
constructor(private vpaId: string) {}
async pay(amount: number): Promise<PaymentReceipt> {
// Isolated UPI execution logic
return { status: "SUCCESS", transactionId: "UPI-4412", amount };
}
}
// Client remains untouched when new payment strategies are introduced
class CheckoutService {
constructor(private paymentStrategy: PaymentStrategy) {}
async completeOrder(cartTotal: number) {
return await this.paymentStrategy.pay(cartTotal);
}
}- In interviews, when an interviewer says 'What if we also need to support cryptocurrency or corporate credits?', immediately highlight that your design satisfies OCP by plugging in a new Strategy class.
- Watch out for the LSP trap: If your derived class throws UnsupportedOperationException (like ReadOnlyList extending List.add()), point out that this violates Liskov Substitution and split the interface instead.
- Creating an interface for every single class even when there is only one implementation and zero anticipated variations (speculative generality).
- Confusing DRY (Don't Repeat Yourself) with SRP: two pieces of code that look identical today may change for completely different business reasons tomorrow.
The Essential Gang of Four Patterns for LLD
While the GoF catalog contains 23 patterns, interviewers focus heavily on 8 core patterns that test your ability to decouple object creation, structure dynamic behavior, and coordinate state changes.
Decouples object creation from consumption. Factory Method handles variants of a single product family, while Abstract Factory creates families of related or dependent objects.
Defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. Standard choice for sorting, pricing, fee calculation, and route optimization.
Establishes a 1-to-N dependency where state changes in the subject trigger automatic notification and updates to all registered observers.
Attaches additional responsibilities to an object dynamically without modifying the underlying class or using subclass explosion. Essential for middleware and stream wrappers.
Allows an object to alter its behavior when its internal state changes. Replaces massive switch-case statements with dedicated state classes implementing common actions.
Ensures a class has only one instance and provides a global point of access. In multi-threaded environments, must be implemented using enum or Bill Pugh holder to prevent race conditions.
Decoupled state broadcasting between Subject and diverse Observer subscribers.
// Method 1: Bill Pugh Initialization-on-Demand Holder (Recommended for Classes)
public class DatabaseConnectionPool {
private DatabaseConnectionPool() {
// Prevent reflection instantiation attack
if (InstanceHolder.INSTANCE != null) {
throw new IllegalStateException("Already initialized");
}
}
private static class InstanceHolder {
// Class loaded only when getInstance() is called; JVM guarantees thread safety
private static final DatabaseConnectionPool INSTANCE = new DatabaseConnectionPool();
}
public static DatabaseConnectionPool getInstance() {
return InstanceHolder.INSTANCE;
}
}
// Method 2: Java Enum Singleton (Guaranteed serialization-safe & thread-safe)
public enum ConfigurationManager {
INSTANCE;
private final Map<String, String> configs = new ConcurrentHashMap<>();
public String getProperty(String key) {
return configs.get(key);
}
public void setProperty(String key, String value) {
configs.put(key, value);
}
}- When designing State-driven machines (Vending Machine, Order Fulfillment, Traffic Signal), never write 'state = NEW_STATE;'. Instead, delegate state transitions to the current state object via 'context.setState(new ProcessingState(context))'.
- For Observer pattern, mention thread-safety: use CopyOnWriteArrayList for observer lists so iteration during event notification doesn't throw ConcurrentModificationException if an observer unregisters.
- Applying design patterns where simple procedural code or a basic function would suffice. Over-engineering with 'AbstractProxyFactoryBuilder' is an immediate red flag.
- Forgetting to unsubscribe observers, creating memory leaks (the Lapsed Listener problem).
Classic Machine Coding Problems & Domain Modeling
In 90-minute machine coding rounds at companies like Flipkart, Uber, Swiggy, and Microsoft, your score depends on object modeling clarity, separation of concerns, and working code.
Core Entities: Vehicle (Bike, Car, Truck), Spot (Compact, Large, EV), Floor, ParkingTicket, FeeStrategy. Key challenge: Spot allocation strategies (nearest to entrance, best fit) and dynamic billing.
Core Entities: ElevatorCar, Direction (UP, DOWN, IDLE), InternalButton, ExternalCall, DispatcherStrategy (SCAN/LOOK algorithm, Nearest Car). Key challenge: Scheduling algorithms and state handling.
Core Entities: CacheEntry, DoublyLinkedList, FrequencyList, EvictionPolicyStrategy. Key challenge: O(1) get/put operations with fine-grained lock or read-write locks.
Core Entities: User, Group, Expense, Split (EqualSplit, ExactSplit, PercentageSplit), BalanceSheet. Key challenge: Debt simplification algorithms (graph transitivity / min cash flow).
export enum VehicleType { MOTORBIKE, CAR, TRUCK }
export enum SpotType { TWO_WHEELER, COMPACT, LARGE }
export class Vehicle {
constructor(public licensePlate: string, public type: VehicleType) {}
}
export class ParkingSpot {
private occupiedVehicle: Vehicle | null = null;
constructor(public id: string, public floor: number, public type: SpotType) {}
isAvailable(): boolean { return this.occupiedVehicle === null; }
park(v: Vehicle) { this.occupiedVehicle = v; }
unpark() { this.occupiedVehicle = null; }
getVehicle() { return this.occupiedVehicle; }
}
export interface SpotAllocationStrategy {
findSpot(spots: ParkingSpot[], vehicle: Vehicle): ParkingSpot | null;
}
export class NearestFirstStrategy implements SpotAllocationStrategy {
findSpot(spots: ParkingSpot[], vehicle: Vehicle): ParkingSpot | null {
return spots.find(s => s.isAvailable() && this.canFit(s.type, vehicle.type)) || null;
}
private canFit(spotType: SpotType, vehicleType: VehicleType): boolean {
if (vehicleType === VehicleType.MOTORBIKE) return true;
if (vehicleType === VehicleType.CAR) return spotType !== SpotType.TWO_WHEELER;
if (vehicleType === VehicleType.TRUCK) return spotType === SpotType.LARGE;
return false;
}
}
export class ParkingLotManager {
private spots: ParkingSpot[] = [];
constructor(private allocationStrategy: SpotAllocationStrategy) {}
addSpot(spot: ParkingSpot) { this.spots.push(spot); }
parkVehicle(vehicle: Vehicle): ParkingSpot {
const spot = this.allocationStrategy.findSpot(this.spots, vehicle);
if (!spot) throw new Error("Parking Lot Full for vehicle type " + vehicle.type);
spot.park(vehicle);
return spot;
}
}- In machine coding, always create an interactive CLI or main driver script that demonstrates sample inputs, prints clear output, and handles invalid commands.
- Model entity IDs cleanly using UUID or autoincrement integer counters to ensure entities can be stored in HashMaps or Lookups easily.
- Writing a single God Manager class containing 500 lines of code with nested maps and arrays instead of object-oriented entity classes.
- Failing to validate inputs: negative prices, null user IDs, and duplicate ticket lookups will cause embarrassing runtime crashes.
Concurrency & Thread-Safety in Low-Level Design
Senior LLD interviews routinely ask: 'What happens when 50 threads call this method at the same millisecond?' You must demonstrate mastery over locks, synchronization, and race condition prevention.
Standard synchronized methods block all concurrent readers and writers. ReentrantReadWriteLock allows multiple concurrent readers while ensuring exclusive writer access, multiplying throughput for read-heavy caches.
AtomicInteger and AtomicReference use hardware Compare-And-Swap (CAS) instructions to achieve lock-free thread safety without thread context switching overhead.
Without the 'volatile' keyword in Java/C++, instruction reordering by the CPU or compiler can expose a partially initialized object to other threads.
Always acquire multiple locks in a globally consistent order (e.g., sort accounts by account ID before locking both in an atomic transfer).
public class ConcurrentLRUCache<K, V> {
private final int capacity;
private final Map<K, Node<K, V>> map;
private final DoublyLinkedList<K, V> list;
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public ConcurrentLRUCache(int capacity) {
this.capacity = capacity;
this.map = new HashMap<>();
this.list = new DoublyLinkedList<>();
}
public V get(K key) {
rwLock.writeLock().lock(); // Write lock needed because LRU order updates on read!
try {
Node<K, V> node = map.get(key);
if (node == null) return null;
list.moveToHead(node);
return node.value;
} finally {
rwLock.writeLock().unlock();
}
}
public void put(K key, V value) {
rwLock.writeLock().lock();
try {
if (map.containsKey(key)) {
Node<K, V> node = map.get(key);
node.value = value;
list.moveToHead(node);
} else {
if (map.size() >= capacity) {
Node<K, V> evicted = list.removeTail();
map.remove(evicted.key);
}
Node<K, V> newNode = new Node<>(key, value);
list.addToHead(newNode);
map.put(key, newNode);
}
} finally {
rwLock.writeLock().unlock();
}
}
}- Always highlight why get() in LRU requires mutation. Pointing out that ReadWriteLock cannot use readLock() for LRU get() immediately sets you apart as a senior engineer who understands real memory structures.
- For deadlocks, articulate the 4 Coffman conditions: Mutual Exclusion, Hold and Wait, No Preemption, and Circular Wait.
- Synchronizing the entire class when only a tiny critical section mutates shared state, causing severe thread contention and throughput collapse.
- Invoking foreign methods (such as observer listener callbacks) while holding a lock, which risks distributed deadlocks.
Refactoring a Monolithic Order Processing Engine to State + Strategy
A fast-growing e-commerce platform experienced frequent production regressions whenever new logistics partners or payment gateways were onboarded, due to a 2,200-line OrderManager with 34 boolean flags.
- 1Identified that order status transitions (CREATED -> PAYMENT_PENDING -> FRAUD_VERIFIED -> DISPATCHED -> DELIVERED -> REFUNDED) were evaluated using nested switch statements.
- 2Decomposed the monolithic class into a State Pattern state machine: each OrderState (CreatedState, PendingPaymentState, DispatchedState) encapsulated allowed transitions and actions.
- 3Extracted courier rate calculation and delivery assignment into a CarrierStrategy (FedExStrategy, DHLStrategy, LocalCourierStrategy) loaded via Factory.
- 4Wrapped payment interactions in an idempotent PaymentCommand pattern supporting transactional rollback / compensation.
Top Must-Know Interview Questions & Model Answers
Q1: How do you choose between Inheritance and Composition in Object-Oriented Design?
- •Inheritance breaks encapsulation because subclass behavior relies heavily on superclass implementation details (the fragile base class problem).
- •Multiple inheritance is either unsupported or causes diamond problem ambiguity in languages like Java, C#, and TypeScript.
- •Composition combined with interfaces allows dynamic dependency injection and mock testing in unit tests.
Q2: What is the difference between Strategy Pattern and State Pattern?
- •Strategy: Usually chosen once by the client at initialization (e.g., CreditCardPayment vs UPIPayment, FastSort vs MergeSort) and the strategies do not know about each other.
- •State: Transitions occur dynamically based on events; concrete states frequently hold a reference to Context and explicitly switch Context to another State (e.g., Pending -> Paid -> Shipped).
- •State encapsulates context-dependent behavior and prevents conditional explosion.
Q3: How do you design a thread-safe in-memory Cache with eviction in a Machine Coding round?
- •In LRU, reads mutate list pointers (moveToHead), meaning get() requires exclusive synchronization or an access-queue approach (like Caffeine cache).
- •In LFU, maintain a FrequencyList where each frequency bucket points to a doubly linked list of nodes with that exact frequency count.
- •Always provide an EvictionStrategy interface so the cache can be configured as LRU, LFU, or FIFO without rewriting core storage.
Q4: How do you prevent the Double-Checked Locking issue in Java Singleton implementations?
- •Without volatile, CPU instruction reordering can perform: 1. Allocate memory -> 2. Assign reference to variable -> 3. Execute constructor.
- •If Thread B checks instance != null while Thread A is between step 2 and 3, Thread B receives a partially initialized object, leading to catastrophic null pointer or corruption bugs.
- •Alternatively, use the Bill Pugh static inner class holder or Java Enum to delegate thread-safe initialization entirely to the JVM classloader.
Q5: How would you design a Rate Limiter library in Low-Level Design?
- •Token Bucket: Store currentTokens, lastRefillTimestamp, capacity, and refillRatePerSecond. On each call, calculate elapsed time, refill tokens proportionally, and decrement if tokens >= 1.
- •Sliding Window: Store timestamps in a Deque<Long>; prune timestamps older than (currentTime - windowSize); check if remaining count < limit.
- •Provide automatic background cleanup of idle client entries using a ScheduledExecutorService to prevent unbounded memory growth.
Mistakes That Sink Otherwise Strong Candidates
Why it happens: Anxiety about the 90-minute clock makes candidates rush to write syntax before understanding the domain model.
The fix: Spend the first 15 minutes asking clarifying questions, listing entities, defining interfaces, and confirming method signatures with the interviewer.
Why it happens: Candidates want to show off knowledge by creating AbstractProxyFactoryBuilders for simple CRUD entities.
The fix: Follow YAGNI (You Aren't Gonna Need It). Introduce a pattern only when it solves a concrete problem of extensibility, coupling, or state coordination.
Why it happens: Subclassing a parent class because it has 90% of the methods needed, then disabling the remaining 10% in the child.
The fix: Split the fat interface into smaller role interfaces (Interface Segregation) or use composition instead of inheritance.
Why it happens: Candidates assume machine coding is single-threaded unless explicitly warned.
The fix: Always ask: 'Should this service handle concurrent requests safely?' Even if not required, mention how you would synchronize state using locks or concurrent collections.
Why it happens: Developers fall back on competitive programming habits of writing everything inside a single main() file.
The fix: Organize code into clear packages: models, services, strategies, repositories, and exceptions. Every class should fit on a single screen.
Quick-Reference Cheat Sheet
Recommended Practice Quizzes on QuizCluster
Test your retention and prepare for timed live coding and MCQ technical screening rounds:
Low-Level Design & SOLID Quiz
Test your ability to spot SOLID violations, choose GoF design patterns, and model clean domain classes.
Java & OOP Internals Quiz
Verify encapsulation, polymorphism, abstract classes vs interfaces, and object lifecycle mechanics.
Concurrency & Multithreading Quiz
Test your mastery over locks, atomic primitives, thread pools, race conditions, and deadlocks.
High-Level System Design Quiz
Transition from object modeling to distributed scalability, caching, sharding, and message queues.
Frequently Asked Questions
What is the difference between Low-Level Design (LLD) and High-Level Design (HLD)?
HLD focuses on distributed system architecture: microservices, databases, load balancers, caching, message brokers, and CAP theorem trade-offs. LLD focuses on software engineering within a single service: object-oriented design, class relationships, design patterns, clean code, SOLID principles, and thread safety.
Which programming language is best for Machine Coding rounds?
Java, C++, and TypeScript/Python are the most accepted. Java is especially favored by enterprise interviewers (Amazon, Flipkart, Uber) because of strict OOP typing, rich concurrent collections (ConcurrentHashMap, BlockingQueue), and clean package structures. Use whichever language you can write idiomatic, bug-free OOP code in quickly.
How do interviewers evaluate Machine Coding rounds?
Interviewers evaluate along 5 pillars: 1. Working Code (does it run and produce expected output?), 2. Object Modeling & Extensibility (are entities well separated?), 3. SOLID & Clean Code, 4. Edge Case & Error Handling, and 5. Time Management & Speed.
Should I write unit tests during a machine coding round?
Yes! Writing 3-5 clean unit tests demonstrating happy paths and edge cases (invalid input, full capacity) shows high engineering maturity and will put you in the top 10% of candidates.