LearnFrenzy
Sign in Take a free test

System Design interview · Set 5 of 6

Low-Level Design (LLD) Coding Questions

  • 6 follow-up questions
  • 4 key points
  • Code examples
Test yourself

Clean classes, clear responsibilities

LLD rounds reward clarifying questions, small focused classes, interfaces for what varies, the right pattern, and a working core flow — plus a word on concurrency.

  • ApproachRequirements, entities, interfaces, code.
  • ClassicsParking lot, rate limiter, Splitwise, elevator.
  • PatternsStrategy, State, Factory, Observer.

Many product companies now run a low-level design (machine coding) round: model a real system with classes, interfaces and patterns, then code the core. Interviewers judge clean responsibilities, extensibility and correctness — the SOLID principles in action.

1. How do you approach an LLD question?

1) Clarify requirements and scope (which features, how many users, concurrency?). 2) Identify entities and their responsibilities. 3) Define interfaces for parts that will change (pricing, strategies). 4) Write the core classes and one end-to-end flow. 5) Discuss extensions, concurrency and patterns used. Keep classes small and names business-like.


2. Design a parking lot.

Entities: ParkingLot, Level, Spot (with a size), Vehicle (with a size), Ticket. A SpotAllocator strategy picks a spot; a PricingPolicy computes fees — both swappable without changing the lot (Open/Closed principle).

enum Size { BIKE, CAR, TRUCK }
record Vehicle(String number, Size size) {}
record Ticket(String id, Vehicle vehicle, Spot spot, Instant entry) {}

class Spot {
    final String id; final Size size; private Vehicle parked;
    Spot(String id, Size size) { this.id = id; this.size = size; }
    boolean fits(Vehicle v) { return parked == null && v.size().ordinal() <= size.ordinal(); }
    void park(Vehicle v) { parked = v; }
    void free() { parked = null; }
}

interface PricingPolicy { long fee(Ticket t, Instant exit); }

class ParkingLot {
    private final List<Spot> spots;
    private final PricingPolicy pricing;
    private final Map<String, Ticket> active = new ConcurrentHashMap<>();
    ParkingLot(List<Spot> spots, PricingPolicy pricing) { this.spots = spots; this.pricing = pricing; }

    synchronized Ticket enter(Vehicle v) {
        Spot spot = spots.stream().filter(s -> s.fits(v)).findFirst()
                         .orElseThrow(() -> new IllegalStateException("Lot full"));
        spot.park(v);
        Ticket t = new Ticket(UUID.randomUUID().toString(), v, spot, Instant.now());
        active.put(t.id(), t);
        return t;
    }
    synchronized long exit(String ticketId) {
        Ticket t = active.remove(ticketId);
        t.spot().free();
        return pricing.fee(t, Instant.now());
    }
}

3. Code a thread-safe token bucket rate limiter.

Each client has a bucket with a capacity and refill rate; a request consumes a token if available. Refill lazily based on elapsed time, and synchronise per bucket.

class TokenBucket {
    private final long capacity; private final double refillPerMs;
    private double tokens; private long last;
    TokenBucket(long capacity, long perSecond) {
        this.capacity = capacity; this.refillPerMs = perSecond / 1000.0;
        this.tokens = capacity; this.last = System.currentTimeMillis();
    }
    synchronized boolean tryConsume() {
        long now = System.currentTimeMillis();
        tokens = Math.min(capacity, tokens + (now - last) * refillPerMs);
        last = now;
        if (tokens < 1) return false;
        tokens -= 1;
        return true;
    }
}

class RateLimiter {
    private final Map<String, TokenBucket> buckets = new ConcurrentHashMap<>();
    boolean allow(String clientId) {
        return buckets.computeIfAbsent(clientId, id -> new TokenBucket(10, 5)).tryConsume();
    }
}

4. Design an expense-sharing app (like Splitwise).

Entities: User, Group, Expense with a SplitStrategy (equal, exact amounts, percentages). Keep a balance sheet of who owes whom; simplify debts by netting balances.

interface SplitStrategy { Map<String, BigDecimal> split(BigDecimal amount, List<String> users); }

class EqualSplit implements SplitStrategy {
    public Map<String, BigDecimal> split(BigDecimal amount, List<String> users) {
        BigDecimal share = amount.divide(BigDecimal.valueOf(users.size()), 2, RoundingMode.HALF_UP);
        Map<String, BigDecimal> out = new LinkedHashMap<>();
        users.forEach(u -> out.put(u, share));
        return out;
    }
}

class Ledger {
    private final Map<String, BigDecimal> net = new HashMap<>();   // positive = is owed money
    void addExpense(String paidBy, BigDecimal amount, List<String> users, SplitStrategy strategy) {
        strategy.split(amount, users).forEach((user, share) -> {
            net.merge(user, share.negate(), BigDecimal::add);
            net.merge(paidBy, share, BigDecimal::add);
        });
    }
    BigDecimal balance(String user) { return net.getOrDefault(user, BigDecimal.ZERO); }
}

5. Design an elevator system.

Entities: Elevator (current floor, direction, stop queue), Request, and a Dispatcher strategy that assigns requests (nearest idle elevator, or one already moving towards the floor). The elevator serves stops in its current direction before reversing (the SCAN algorithm), which avoids starvation.


6. Which design patterns appear most in LLD rounds?

  • Strategy — pricing, splitting, allocation rules.
  • Factory — creating vehicles, notifications, payment methods by type.
  • Observer — notify users when an order status changes.
  • State — vending machines, order lifecycles.
  • Singleton (sparingly) — configuration, ID generator.
  • Decorator — add toppings or features without subclass explosion.
  • Builder — complex objects with many optional fields.

7. Model a vending machine with the State pattern.

Each state (Idle, HasMoney, Dispensing, OutOfStock) handles actions differently, so the machine has no giant if-else blocks and new states are easy to add.

interface State { void insertCoin(VendingMachine m, int amount); void select(VendingMachine m, String code); }

class Idle implements State {
    public void insertCoin(VendingMachine m, int amount) { m.addBalance(amount); m.setState(new HasMoney()); }
    public void select(VendingMachine m, String code) { System.out.println("Insert money first"); }
}

class HasMoney implements State {
    public void insertCoin(VendingMachine m, int amount) { m.addBalance(amount); }
    public void select(VendingMachine m, String code) {
        if (m.price(code) > m.balance()) { System.out.println("Add more money"); return; }
        m.dispense(code);
        m.setState(new Idle());
    }
}

8. How do you handle concurrency in an LLD answer?

Identify shared mutable state (spots, seats, balances), protect it with the smallest safe scope (synchronised methods, locks per object, atomic counters or concurrent maps), and prefer immutable value objects. For bookings, mention optimistic locking with version numbers when the state lives in a database.


Remember

Key points

  • Interfaces for what changes

    Pricing, allocation, splitting.

  • Strategy beats if-else

    Open for extension.

  • BigDecimal for money

    Never double.

  • Protect shared state

    Locks, atomics or concurrent maps.

Interview drill

Follow-up questions

What the interviewer asks after your first answer. Turn on practice mode, answer each one aloud, then reveal.

  1. 1 How would you add an EV charging spot to the parking lot? Intermediate

    Add a spot feature or subtype and let the allocator strategy prefer it for electric vehicles; pricing adds a charging component — no changes to ParkingLot itself.

  2. 2 How do you design a BookMyShow seat booking without double booking? Advanced

    Hold seats with a short expiry (a lock or reservation row), confirm on payment, and rely on a unique constraint or optimistic version check so concurrent bookings fail cleanly.

  3. 3 What is the difference between composition and inheritance in LLD? Basic

    Composition assembles behaviour from collaborating objects and is easier to change; inheritance shares behaviour through a hierarchy and becomes rigid when overused.

  4. 4 How do you design an in-memory key-value store with TTL? Intermediate

    A ConcurrentHashMap of entries with expiry times, lazy expiry on reads plus a background cleaner (or a delay queue) for expired keys.

  5. 5 How would you make the rate limiter distributed? Advanced

    Store counters in Redis and update them atomically (Lua script or INCR with expiry) so all servers share the same limits.

  6. 6 Observer vs pub/sub? Intermediate

    Observer is an in-process pattern where subjects call observers directly; pub/sub decouples publishers and subscribers through a broker, often across processes.

Preparing with friends?

Share this page with your study group or placement batch.

Popular