What is LLD?
Low Level Design (LLD) is the art of translating requirements into working code architecture — thinking deeply about how classes, objects, and components interact with each other.
Defining classes, their attributes, methods, and access modifiers with clear responsibility.
How classes relate — inheritance, composition, aggregation, and association.
Reusable solutions to common design problems — Singleton, Factory, Observer, etc.
LLD: "The ATM has a
CardReader class, a CashDispenser class, a Transaction class. The Transaction class has methods withdraw(), deposit(), getBalance(). The CardReader validates via AuthenticationService..."
Why LLD Matters
Maintainability
Well-designed code is easy to change without breaking other parts. Good LLD means you can add features without rewriting everything.
Scalability
Proper design patterns allow the system to grow. Adding a new payment method shouldn't require touching existing payment code.
Reusability
Good LLD creates components that can be used across different parts of the system without copy-pasting code.
Testability
Well-designed classes are easy to unit test. Tight coupling makes testing nearly impossible.
OOP Foundations
Object-Oriented Programming is the bedrock of LLD. Master these 4 pillars and everything else becomes intuitive.
public class BankAccount { // Private — hidden from outside world private String accountNumber; private double balance; private String pin; public BankAccount(String accNum, String pin, double initialBalance) { this.accountNumber = accNum; this.pin = pin; this.balance = initialBalance; } // Public methods — the controlled interface public boolean withdraw(double amount, String pin) { if (!validatePin(pin)) return false; if (amount > balance) return false; balance -= amount; return true; } public double getBalance(String pin) { if (!validatePin(pin)) throw new SecurityException("Invalid PIN"); return balance; } // Private — internal logic hidden private boolean validatePin(String inputPin) { return this.pin.equals(inputPin); } }
abstract class Vehicle { protected String brand; protected int speed; public Vehicle(String brand) { this.brand = brand; } public void start() { System.out.println(brand + " engine starting..."); } public abstract void fuelUp(); // must be implemented by subclasses } class Car extends Vehicle { private boolean hasSunroof; public Car(String brand, boolean sunroof) { super(brand); // calls parent constructor this.hasSunroof = sunroof; } @Override public void fuelUp() { System.out.println("Filling petrol at gas station"); } public void park() { System.out.println(brand + " is parking"); } } class ElectricCar extends Car { private int batteryCapacity; @Override public void fuelUp() { System.out.println("Charging battery..."); } }
abstract class Shape { public abstract double area(); // each shape implements differently public abstract double perimeter(); } class Circle extends Shape { private double radius; public Circle(double r) { this.radius = r; } @Override public double area() { return Math.PI * radius * radius; } @Override public double perimeter() { return 2 * Math.PI * radius; } } class Rectangle extends Shape { private double width, height; public Rectangle(double w, double h) { this.width=w; this.height=h; } @Override public double area() { return width * height; } @Override public double perimeter() { return 2 * (width + height); } } // Polymorphism in action! List<Shape> shapes = new ArrayList<>(); shapes.add(new Circle(5)); shapes.add(new Rectangle(4, 6)); for (Shape s : shapes) { // JVM decides at runtime which area() to call System.out.println("Area: " + s.area()); }
// Interface: Pure abstraction — defines WHAT, not HOW interface PaymentGateway { boolean processPayment(double amount, String currency); boolean refund(String transactionId); String getTransactionStatus(String id); } // Concrete implementations — the HOW class StripeGateway implements PaymentGateway { @Override public boolean processPayment(double amount, String currency) { // Stripe-specific API calls... return true; } // ... other implementations } class PayPalGateway implements PaymentGateway { @Override public boolean processPayment(double amount, String currency) { // PayPal-specific API calls... return true; } } // Client code doesn't care about implementation! class CheckoutService { private PaymentGateway gateway; // uses interface type public CheckoutService(PaymentGateway gateway) { this.gateway = gateway; } public void checkout(double amount) { gateway.processPayment(amount, "USD"); // works with any gateway! } }
PaymentGateway interface, you can swap Stripe for PayPal without touching CheckoutService. This is the power of abstraction combined with dependency injection.SOLID Principles
Five design principles that make software more understandable, flexible, and maintainable. Every senior engineer swears by these.
S — Single Responsibility (Deep Dive)
- UserService handles login logic
- UserService also sends emails
- UserService also formats user data
- UserService also writes to database
- AuthService → login/logout
- EmailService → send emails
- UserFormatter → format data
- UserRepository → database ops
// ❌ BAD: This class has TOO MANY responsibilities class UserService_BAD { public void registerUser(User user) { /* ... */ } public void sendWelcomeEmail(User user) { /* ... */ } // wrong place! public void saveToDatabase(User user) { /* ... */ } // wrong place! public String generateReport() { /* ... */ } // wrong place! } // ✅ GOOD: Each class has ONE job class UserService { // Only handles user business logic private UserRepository repo; private EmailService emailSvc; public void registerUser(User user) { repo.save(user); // delegates DB work emailSvc.sendWelcome(user); // delegates email work } } class UserRepository { // Only handles database operations public void save(User user) { /* DB logic */ } public User findById(long id) { /* DB logic */ return null; } } class EmailService { // Only handles email sending public void sendWelcome(User user) { /* Email logic */ } public void sendReset(User user) { /* Email logic */ } }
O — Open/Closed Principle (Deep Dive)
if-else block per discount type — every new discount requires modifying existing code. Good design: a DiscountStrategy interface — just add a new class for each new discount!// Open for extension (add new classes) ✅ // Closed for modification (don't touch this interface) ✅ interface DiscountStrategy { double apply(double price); } class SeasonalDiscount implements DiscountStrategy { @Override public double apply(double price) { return price * 0.80; } // 20% off } class LoyaltyDiscount implements DiscountStrategy { @Override public double apply(double price) { return price * 0.90; } // 10% off } // Adding a new discount? Just add a new class — no existing code touched! class CouponDiscount implements DiscountStrategy { private double couponAmount; public CouponDiscount(double amount) { this.couponAmount = amount; } @Override public double apply(double price) { return price - couponAmount; } } class PriceCalculator { public double calculate(double price, DiscountStrategy discount) { return discount.apply(price); // never changes regardless of discount type } }
DRY · KISS · YAGNI
Three golden rules that every professional developer follows to keep code clean and sane.
Every piece of knowledge must have a single, unambiguous, authoritative representation in the system. If you copy-paste code, you're violating DRY.
Most systems work best if they are kept simple rather than made complicated. Prefer simple solutions over clever ones.
Don't add functionality until it's actually needed. Premature optimization and over-engineering are the enemy.
// ❌ WET (Write Everything Twice) — DON'T do this public String getFullNameForEmail(User user) { return user.getFirstName() + " " + user.getLastName(); } public String getFullNameForReport(User user) { return user.getFirstName() + " " + user.getLastName(); // duplicated! } public String getFullNameForInvoice(User user) { return user.getFirstName() + " " + user.getLastName(); // duplicated again! } // ✅ DRY — Single source of truth public String getFullName(User user) { return user.getFirstName() + " " + user.getLastName(); } // Now every place calls getFullName() — change once, fixed everywhere!
Creational Patterns
Patterns that deal with object creation mechanisms. They provide ways to create objects while hiding the creation logic.
public class DatabaseConnection { // volatile ensures visibility across threads private static volatile DatabaseConnection instance = null; // Private constructor — no one can create it from outside private DatabaseConnection() { // Expensive initialization: connect to DB, setup pools... System.out.println("DB Connection established"); } // Double-checked locking for thread safety public static DatabaseConnection getInstance() { if (instance == null) { synchronized (DatabaseConnection.class) { if (instance == null) { instance = new DatabaseConnection(); } } } return instance; } public void query(String sql) { /* execute query */ } } // Usage DatabaseConnection db1 = DatabaseConnection.getInstance(); DatabaseConnection db2 = DatabaseConnection.getInstance(); // db1 == db2 → ALWAYS TRUE
interface Notification { void send(String message); } class EmailNotification implements Notification { @Override public void send(String message) { System.out.println("📧 Email: " + message); } } class SMSNotification implements Notification { @Override public void send(String message) { System.out.println("📱 SMS: " + message); } } class PushNotification implements Notification { @Override public void send(String message) { System.out.println("🔔 Push: " + message); } } // The Factory — knows which object to create class NotificationFactory { public static Notification create(String type) { switch (type.toLowerCase()) { case "email": return new EmailNotification(); case "sms": return new SMSNotification(); case "push": return new PushNotification(); default: throw new IllegalArgumentException("Unknown type: " + type); } } } // Client code Notification n = NotificationFactory.create("email"); n.send("Your order has shipped!");
public class Pizza { private String size; private String crust; private String sauce; private List<String> toppings; private boolean extraCheese; // Private — only Builder can create this private Pizza(Builder builder) { this.size = builder.size; this.crust = builder.crust; this.sauce = builder.sauce; this.toppings = builder.toppings; this.extraCheese = builder.extraCheese; } public static class Builder { private String size; private String crust = "thin"; // defaults private String sauce = "tomato"; private List<String> toppings = new ArrayList<>(); private boolean extraCheese = false; public Builder(String size) { this.size = size; } public Builder crust(String crust) { this.crust = crust; return this; } public Builder sauce(String sauce) { this.sauce = sauce; return this; } public Builder topping(String t) { toppings.add(t); return this; } public Builder extraCheese() { this.extraCheese = true; return this; } public Pizza build() { return new Pizza(this); } } } // Clean, readable object creation! Pizza myPizza = new Pizza.Builder("large") .crust("thick") .sauce("bbq") .topping("pepperoni") .topping("mushrooms") .extraCheese() .build();
Structural Patterns
Patterns that deal with object composition — how classes and objects are composed to form larger structures.
// Your code expects this interface interface MediaPlayer { void play(String filename); } // But you have this third-party library with different interface class VlcPlayer { public void playVlc(String filename) { System.out.println("VLC playing: " + filename); } } // Adapter bridges the gap class VlcAdapter implements MediaPlayer { private VlcPlayer vlcPlayer; public VlcAdapter(VlcPlayer player) { this.vlcPlayer = player; } @Override public void play(String filename) { vlcPlayer.playVlc(filename); // translates the call } } // Client uses MediaPlayer interface — doesn't know about VLC! MediaPlayer player = new VlcAdapter(new VlcPlayer()); player.play("movie.vlc");
interface Coffee { String getDescription(); double getCost(); } class Espresso implements Coffee { @Override public String getDescription() { return "Espresso"; } @Override public double getCost() { return 1.99; } } // Abstract decorator — wraps a Coffee abstract class CoffeeDecorator implements Coffee { protected Coffee coffee; public CoffeeDecorator(Coffee c) { this.coffee = c; } } class MilkDecorator extends CoffeeDecorator { public MilkDecorator(Coffee c) { super(c); } @Override public String getDescription() { return coffee.getDescription() + ", Milk"; } @Override public double getCost() { return coffee.getCost() + 0.50; } } class CaramelDecorator extends CoffeeDecorator { public CaramelDecorator(Coffee c) { super(c); } @Override public String getDescription() { return coffee.getDescription() + ", Caramel"; } @Override public double getCost() { return coffee.getCost() + 0.75; } } // Stacking decorators! Coffee myCoffee = new Espresso(); myCoffee = new MilkDecorator(myCoffee); myCoffee = new CaramelDecorator(myCoffee); System.out.println(myCoffee.getDescription()); // Espresso, Milk, Caramel System.out.println("$" + myCoffee.getCost()); // $3.24
Behavioral Patterns
Patterns that deal with object interaction and responsibilities — how objects communicate and collaborate.
interface Observer { void update(String event, Object data); } interface Subject { void subscribe(Observer observer); void unsubscribe(Observer observer); void notifyObservers(String event, Object data); } class OrderService implements Subject { private List<Observer> observers = new ArrayList<>(); @Override public void subscribe(Observer o) { observers.add(o); } @Override public void unsubscribe(Observer o) { observers.remove(o); } @Override public void notifyObservers(String event, Object data) { for (Observer o : observers) o.update(event, data); } public void placeOrder(Order order) { // ... process order logic ... notifyObservers("ORDER_PLACED", order); // broadcast! } } // Multiple independent observers react to the same event class EmailNotifier implements Observer { @Override public void update(String event, Object data) { if ("ORDER_PLACED".equals(event)) System.out.println("Sending confirmation email..."); } } class InventoryManager implements Observer { @Override public void update(String event, Object data) { if ("ORDER_PLACED".equals(event)) System.out.println("Updating inventory..."); } }
interface SortStrategy { void sort(int[] array); } class BubbleSort implements SortStrategy { @Override public void sort(int[] arr) { // O(n²) — good for small arrays System.out.println("Bubble sorting..."); } } class QuickSort implements SortStrategy { @Override public void sort(int[] arr) { // O(n log n) average — good for large arrays System.out.println("Quick sorting..."); } } // Context — uses a strategy class DataSorter { private SortStrategy strategy; public DataSorter(SortStrategy strategy) { this.strategy = strategy; } // Can switch strategy at runtime! public void setStrategy(SortStrategy s) { this.strategy = s; } public void performSort(int[] data) { strategy.sort(data); } } // Usage DataSorter sorter = new DataSorter(new BubbleSort()); sorter.performSort(smallArray); sorter.setStrategy(new QuickSort()); // switch at runtime! sorter.performSort(largeArray);
UML & Class Diagrams
UML (Unified Modeling Language) is the standard visual language for designing and communicating software architecture. Class diagrams are the most important type for LLD.
Relationship Types
| Relationship | Symbol | Meaning | Example |
|---|---|---|---|
| Inheritance | ──△ (solid + open arrow) | IS-A relationship | Dog IS-A Animal |
| Implementation | - - △ (dashed + open arrow) | Implements interface | Dog implements Runnable |
| Composition | ──◆ (solid diamond) | HAS-A (strong ownership) | Car HAS-A Engine (engine dies with car) |
| Aggregation | ──◇ (hollow diamond) | HAS-A (weak ownership) | Team HAS-A Player (player exists without team) |
| Association | ──→ (solid arrow) | USES-A relationship | Teacher USES-A Student |
| Dependency | - - → (dashed arrow) | Depends on temporarily | Method takes Object as param |
Complete Class Diagram — Library System
Real-World Case Studies
Apply everything you've learned to real LLD interview problems. These are the most commonly asked questions in product companies.
Parking Lot System
Key Classes Identified
ParkingFloor
ParkingSpot
Vehicle
Ticket
VehicleType
SpotStatus
TicketStatus
Strategy (Pricing)
Factory (Spot create)
enum SpotType { SMALL, MEDIUM, LARGE, HANDICAPPED } enum VehicleType { MOTORCYCLE, CAR, TRUCK } abstract class Vehicle { protected String licensePlate; protected VehicleType type; public abstract boolean canFitInSpot(ParkingSpot spot); } class Car extends Vehicle { public Car(String plate) { this.licensePlate = plate; this.type = VehicleType.CAR; } @Override public boolean canFitInSpot(ParkingSpot spot) { return spot.getType() == SpotType.MEDIUM || spot.getType() == SpotType.LARGE; } } class ParkingSpot { private String spotId; private SpotType type; private boolean occupied = false; private Vehicle parkedVehicle; public ParkingSpot(String id, SpotType type) { this.spotId = id; this.type = type; } public boolean park(Vehicle vehicle) { if (occupied || !vehicle.canFitInSpot(this)) return false; parkedVehicle = vehicle; occupied = true; return true; } public void unpark() { parkedVehicle = null; occupied = false; } public SpotType getType() { return type; } public boolean isOccupied() { return occupied; } } class ParkingFloor { private List<ParkingSpot> spots; private int floorNumber; public ParkingSpot findAvailableSpot(Vehicle v) { return spots.stream() .filter(s -> !s.isOccupied() && v.canFitInSpot(s)) .findFirst() .orElse(null); } } // Singleton Parking Lot class ParkingLot { private static ParkingLot instance; private List<ParkingFloor> floors; private Map<String, Ticket> activeTickets = new HashMap<>(); private ParkingLot() {} public static ParkingLot getInstance() { if (instance == null) instance = new ParkingLot(); return instance; } public Ticket entry(Vehicle vehicle) { for (ParkingFloor floor : floors) { ParkingSpot spot = floor.findAvailableSpot(vehicle); if (spot != null) { spot.park(vehicle); Ticket ticket = new Ticket(vehicle, spot); activeTickets.put(ticket.getId(), ticket); return ticket; } } throw new RuntimeException("Parking lot is full!"); } }
Elevator System
enum Direction { UP, DOWN, IDLE } enum ElevatorState { MOVING, DOOR_OPEN, IDLE } class Elevator { private int id; private int currentFloor = 1; private Direction direction = Direction.IDLE; private ElevatorState state = ElevatorState.IDLE; private TreeSet<Integer> floorQueue = new TreeSet<>(); public void addRequest(int floor) { floorQueue.add(floor); if (state == ElevatorState.IDLE) processRequests(); } private void processRequests() { while (!floorQueue.isEmpty()) { int nextFloor; if (direction == Direction.UP || direction == Direction.IDLE) { // Get next floor above (SCAN algorithm) Integer above = floorQueue.ceiling(currentFloor); nextFloor = (above != null) ? above : floorQueue.first(); } else { Integer below = floorQueue.floor(currentFloor); nextFloor = (below != null) ? below : floorQueue.last(); } moveToFloor(nextFloor); floorQueue.remove(nextFloor); } direction = Direction.IDLE; state = ElevatorState.IDLE; } private void moveToFloor(int targetFloor) { direction = (targetFloor > currentFloor) ? Direction.UP : Direction.DOWN; state = ElevatorState.MOVING; currentFloor = targetFloor; // Open door, wait, close door state = ElevatorState.DOOR_OPEN; } public int getCurrentFloor() { return currentFloor; } public ElevatorState getState() { return state; } } // Dispatcher — assigns requests to optimal elevator class ElevatorController { private List<Elevator> elevators; public void requestElevator(int floor) { // Find nearest idle or same-direction elevator Elevator best = elevators.stream() .filter(e -> e.getState() == ElevatorState.IDLE) .min(Comparator.comparingInt( e -> Math.abs(e.getCurrentFloor() - floor))) .orElse(elevators.get(0)); best.addRequest(floor); } }
Chess Game
enum Color { WHITE, BLACK } enum GameStatus { ACTIVE, CHECK, CHECKMATE, STALEMATE, RESIGN } class Position { private int row, col; // 0-7 public Position(int r, int c) { row = r; col = c; } public boolean isValid() { return row >= 0 && row < 8 && col >= 0 && col < 8; } } // Abstract piece — polymorphism for move validation abstract class Piece { protected Color color; protected Position position; protected boolean hasMoved = false; public abstract List<Position> getValidMoves(Board board); public abstract String getSymbol(); } class King extends Piece { @Override public List<Position> getValidMoves(Board board) { List<Position> moves = new ArrayList<>(); int[][] dirs = {{-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1}}; for (int[] d : dirs) { Position p = new Position(position.row+d[0], position.col+d[1]); if (p.isValid() && !board.hasFriendlyPiece(p, color)) moves.add(p); } return moves; } @Override public String getSymbol() { return color==Color.WHITE ? "♔" : "♚"; } } class Board { private Piece[][] grid = new Piece[8][8]; public Piece getPiece(Position p) { return grid[p.row][p.col]; } public boolean hasFriendlyPiece(Position p, Color color) { Piece piece = getPiece(p); return piece != null && piece.color == color; } public boolean isKingInCheck(Color kingColor) { Position kingPos = findKing(kingColor); // Check if any enemy piece can reach kingPos for (int r = 0; r < 8; r++) { for (int c = 0; c < 8; c++) { Piece p = grid[r][c]; if (p != null && p.color != kingColor) { if (p.getValidMoves(this).contains(kingPos)) return true; } } } return false; } }
Interview Tips & Cheat Sheet
Everything you need to ace an LLD interview at any top tech company.
How to Approach an LLD Interview
Clarify Requirements (3-5 min)
Ask about scope: who are the users? What are the core features? What are we NOT building? Scale estimates? Always ask before coding.
Identify Core Entities (3-5 min)
List the nouns from requirements — these become your classes. E.g., for a hotel system: Hotel, Room, Guest, Booking, Payment.
Define Relationships (5 min)
Draw a quick class diagram on a whiteboard. Show IS-A, HAS-A relationships. Use correct UML notation.
Identify Design Patterns (2-3 min)
Which patterns apply? Singleton for config/managers, Observer for events, Strategy for algorithms, Factory for object creation.
Code Core Classes (15-20 min)
Start with interfaces and abstract classes. Write concrete implementations. Show enums. Demonstrate the main flow.
Discuss Extensions & Trade-offs
What would change if we needed X feature? How would you handle concurrency? What are the limitations of your design?
Pattern Recognition Cheat Sheet
| Situation | Pattern | Example |
|---|---|---|
| Need only ONE instance | Singleton | Logger, Config, DB Connection |
| Creating objects without specifying exact class | Factory | Notifications, Documents, Parsers |
| Building complex objects step by step | Builder | SQL queries, HTTP requests, UI forms |
| Incompatible interfaces need to work together | Adapter | Third-party APIs, Legacy code |
| Add behavior dynamically without subclassing | Decorator | Streams, Coffee toppings, Middleware |
| One event, many listeners | Observer | Event systems, UI listeners, Pub/Sub |
| Interchangeable algorithms | Strategy | Sorting, Payment, Navigation |
| Object changes behavior based on state | State | Elevator, Vending machine, Order status |
Common LLD Interview Questions
- 🅿️ Design a Parking Lot
- 🏢 Design an Elevator System
- 📚 Design Library Management
- ♟️ Design Chess / Snake & Ladder
- 🏨 Design Hotel Booking System
- 🛒 Design an E-commerce Cart
- 🍔 Design Splitwise / Bill Splitter
- 🚗 Design Ride Sharing (Uber)
- 🎬 Design Movie Ticket Booking
- 💬 Design Chat Application
- 🏦 Design ATM / Banking System
- 📊 Design a Logging Framework