LLD Cohort · Week 5 · Sun 19 Apr 2026 · recording 2h52m

LLD Framework, HashMap Internals & LRU Intuition

The 5-step framework for any LLD interview, the list of problems the course will cover, a look inside a HashMap (hashing, buckets, chaining, load factor, rehashing), and the basic idea of an LRU cache.

Each topic: one-line idea → the problem → real-life picture → the design step by step → Python code → doubts & corrections → remember

The LLD interview framework ▶ 1:18:37 – 1:41:07

Five steps to follow for any "Design X" question: Requirements → Entities → Relationships → Code → Edge cases & extensions.

The problem

In an LLD round (usually 45–60 minutes) you get a one-line prompt: "Design a parking lot", "Design a rate limiter", "Design an LRU cache", "Design an elevator", "Design a vending machine", "Design a notification system". Many candidates jump straight into code because they "know" the problem, and they fail. These rounds are highly interactive. The interviewer is grading how you think: first principles or jumping to solutions, whether you look for edge cases, how clearly you explain, and whether your comments and names are clean.

Real-life picture

An architect does not start pouring concrete when a client says "build me a house". They first ask how many floors and rooms, then list the rooms, then draw how the rooms connect, and only then build. In LLD, entities are the rooms, relationships are the doors and corridors, and code is the construction.

The checklist

#StepWhat you actually doParking-lot example
1Clarify requirementsAsk questions. Write down functional requirements (what the system does) and non-functional requirements (how well it does it).FR: multi-storey? Which vehicle types? Several entry and exit gates? How are tickets and payments handled? NFR: thread-safe slot allocation, extensible to new vehicle types, SOLID, low coupling.
2Identify core entities, enums, interfacesOnly list the classes you plan to create. Find the nouns in your requirements.Classes: ParkingLot, ParkingFloor, ParkingSlot, EntranceGate, ExitGate, Ticket, Payment, Vehicle (abstract). Enums: VehicleType, SlotType.
3Define relationshipsSay how the entities are connected: is-a (inheritance), has-a (composition or aggregation), uses (association). This decides which fields each class holds.ParkingLot has a list of ParkingFloors. A floor has slots and a display board. The entrance gate issues a Ticket. A Ticket has a Vehicle. The exit gate starts the Payment. Car is a Vehicle.
4Start codingBefore you type, say in one line which design patterns you plan to use, and only if you are confident. Then code without wasting time."I'll use Strategy for slot allocation and pricing, and Factory for vehicles."
5Discuss edge cases & extensibilityConcurrency (two cars, one slot), failures, invalid input, and how a new feature would plug in.Lot full, lost ticket, adding an EV-charging slot type.

Step 1 in detail: functional vs non-functional

Step 2 in detail: class or enum?

Step 3 in detail: why relationships matter

A relationship turns into a field. You can only write vehicle inside Ticket if you have decided "a Ticket has a Vehicle". Relationships also tell you about lifecycle: if A is deleted, does B die too (composition) or survive (aggregation)? This was a student's answer in class. To find a relationship, ask a plain question: "Does a parking lot hold floors, or do floors hold lots?" Not everything must be linked directly. ParkingLot and Payment are only related through ExitGate, so Payment is like a "grandchild" of the lot.

classDiagram
  class ParkingLot {
    +id
    +address
    +floors
    +entrance_gates
    +exit_gates
  }
  class ParkingFloor {
    +slots
    +display_board
  }
  class ParkingSlot {
    +slot_type
    +is_free
  }
  class EntranceGate {
    +issue_ticket()
  }
  class ExitGate {
    +process_payment()
  }
  class Ticket {
    +vehicle
    +entry_time
  }
  class Vehicle {
    +number
    +vehicle_type
  }
  ParkingLot o-- ParkingFloor
  ParkingFloor o-- ParkingSlot
  ParkingLot o-- EntranceGate
  ParkingLot o-- ExitGate
  EntranceGate --> Ticket
  Ticket --> Vehicle
  ExitGate --> Payment
  Car --|> Vehicle
  Truck --|> Vehicle
  Motorcycle --|> Vehicle

This is a sketch of the parking-lot diagram the teacher showed. In the interview you will usually have only a code editor, so you say these relationships out loud or type them as comments. You do not draw them.

A tiny skeleton you can type in the first 10 minutes

Python
from abc import ABC
from dataclasses import dataclass, field
from enum import Enum


# Step 2: enums for "types" (no behaviour, just readable names)
class VehicleType(Enum):
    CAR = "car"
    TRUCK = "truck"
    MOTORCYCLE = "motorcycle"


class SlotType(Enum):
    SMALL = "small"
    MEDIUM = "medium"
    LARGE = "large"


# Step 2: entities = classes (state + behaviour); Step 3: relationships become fields
class Vehicle(ABC):                        # Car IS-A Vehicle (inheritance)
    def __init__(self, number: str, vehicle_type: VehicleType):
        self.number = number
        self.vehicle_type = vehicle_type


class Car(Vehicle):
    def __init__(self, number: str):
        super().__init__(number, VehicleType.CAR)


@dataclass
class ParkingSlot:
    slot_id: int
    slot_type: SlotType
    vehicle: Vehicle | None = None         # a slot HAS-A (maybe) vehicle

    def is_free(self) -> bool:
        return self.vehicle is None


@dataclass
class ParkingFloor:
    number: int
    slots: list[ParkingSlot] = field(default_factory=list)   # floor HAS slots


@dataclass
class ParkingLot:
    address: str
    floors: list[ParkingFloor] = field(default_factory=list)  # lot HAS floors


# usage
lot = ParkingLot("MG Road", [ParkingFloor(0, [ParkingSlot(1, SlotType.MEDIUM)])])
slot = lot.floors[0].slots[0]
slot.vehicle = Car("KA-01-1234")
print(slot.is_free(), slot.vehicle.vehicle_type.name)   # False CAR
Remember

R-E-R-C-E: Requirements (FR + NFR) → Entities/enums/interfaces → Relationships → Code (name your patterns first) → Edge cases & extensions.

Common mistake:

Do not name-drop patterns you cannot defend. The teacher's rule: mention Strategy, Observer or Decorator before coding only if you are confident. If time runs out, the interviewer still knows you would have used them.

Doubt: What is an "entity" in LLD?
→

A class: something with state (fields) and behaviour (methods). Things that are only "types" or "states" become enums.

Doubt: Why bother with relationships before coding?
→

Objects talk to each other, and lifecycles depend on each other. If you delete a parent, do its children go too? The relationships decide which object holds which other object as a field. Without them you cannot write the classes. (Answer from Payal, confirmed by the teacher.)

Key takeaways
  • LLD rounds test thinking and communication as much as code. Keep talking with the interviewer.
  • Split requirements into functional (features) and non-functional (thread safety, extensibility, SOLID).
  • Entity = class (state + behaviour). Types and states = enums. Behaviour that varies = interfaces.
  • Relationships become fields. Settle them before writing classes.
  • Announce your design patterns before coding, then finish with edge cases and extensions.

Course roadmap: the LLD problems to practise ▶ 1:47:41 – 2:02:50

About 15 problems that cover around 80% of LLD interviews. Each one teaches a different modelling skill.

#ProblemWhat it teaches
1Parking lotThe classic. Entities, enums, relationships. (One student failed it at KPMG.)
2Elevator systemState and scheduling
3Library managementCRUD-style design with central management of users and items. Also stands in for "banking-like" systems.
4Ride sharing (Uber/Ola)Strategy, state machine, real-time matching, concurrency
5Hotel bookingConcurrency and locking on shared inventory. The teacher said concurrency is "an extra 20 marks".
6Chess (plus tic-tac-toe and snake & ladder, already done)Board-game modelling
7ATM machineState pattern
8Food ordering (Swiggy/Zomato)A frequently asked e-commerce-style flow
9Shopping cart / e-commerceA favourite at Amazon
10Notification systemExecutor service, thread pools
11Rate limiterSimple LLD, but the algorithms matter
12Load balancerBalancing algorithms (consistent hashing may be covered here)
13Thread poolHard to invent on the spot if you have not seen it
14+Stock trading, Splitwise, URL shortener, distributed / async queue (pub-sub)Newer or suggested additions

HashMap and LRU cache are "starter" questions. They lean more on data structures than on LLD, but you will meet one of them in roughly 1 out of 10 interviews. Social-media design was left out on purpose: as LLD it is mostly CRUD, and the interesting part (friend suggestions) is an HLD or data-structures problem.

Homework

Implement HashMap and LRU cache yourself, and read up on the parking lot (next week's first problem).

LLD of HashMap ▶ 2:02:50 – 2:42:48

A key-value store that uses a hash function to turn any key into an array index, so lookups are O(1) on average.

The problem

We want lookup by a unique key to be very fast, ideally O(1), even with millions of entries. Interviewers ask this to see whether you know what happens "under the hood". The teacher's claim: 80% of engineers cannot write one.

Why arrays are the secret

An array jumps to any index in O(1) with simple arithmetic: address(arr[i]) = base + i × size_of_element. This is called direct addressing. If keys were always small integers 0..n-1, we could store value at arr[key] and be done (4 → "Ajay", 3 → "Pradeep", 2 → "Upasana"). Real keys are not like that. They are big numbers like 981111, names, or whole documents.

Real-life picture

A gym fingerprint scanner. Your fingerprint (a huge, messy key) is turned into a code that points to your record. The same finger must always give the same code. That conversion is the hash function.

Building it step by step

  1. Hash function: a formula that takes a key and returns an index in 0 … table_size - 1. Typically index = hash(key) % capacity. A good hash function is deterministic (same input, same output), spreads keys evenly, and is fast.
  2. Collision: with more keys than slots, two keys will sometimes land on the same index. With 4 slots and 10 keys, this is guaranteed.
  3. Chaining: each slot (a bucket) holds the head of a linked list. A colliding key is added to that chain. The bucket array is an array of Entry references, much like an adjacency list in graphs.
  4. Worst case: every key hashes to the same bucket. That gives one long chain, and lookup becomes O(n).
  5. Load factor (α): α = number of elements / table size, which is the average chain length. 8 items in 4 buckets gives α = 2. 14 items in 4 buckets gives α = 3.5.
  6. Rehashing (resizing): when α passes a threshold (0.75, chosen from experiments), the table is "too small". Double the capacity (4 → 8 → 16 …), go through every entry, and re-place it using the new % capacity. For example, capacity 16 × 0.75 = 12, so Java's HashMap resizes when the 13th element is inserted.

Python code

The Entry node stores key, value, a cached hash and next. MyHashMap holds the bucket array, capacity, size and load factor. No dict is used for storage.

Python
from typing import Any, Optional


class Entry:
    """One node in a bucket's chain (a singly linked list)."""
    def __init__(self, key: Any, value: Any, h: int, nxt: Optional["Entry"] = None):
        self.key = key
        self.value = value
        self.hash = h          # cached hash: no need to recompute it while resizing
        self.next = nxt


class MyHashMap:
    DEFAULT_CAPACITY = 16
    LOAD_FACTOR = 0.75

    def __init__(self, capacity: int = DEFAULT_CAPACITY):
        self.capacity = capacity
        self.table: list[Optional[Entry]] = [None] * capacity   # the bucket array
        self.size = 0                                           # entries stored

    def _index(self, h: int) -> int:
        # squeeze any hash into a slot between 0 and capacity - 1
        return h % self.capacity

    def put(self, key: Any, value: Any) -> None:
        # Only put adds elements, so only put checks the load factor
        if self.size + 1 > self.capacity * self.LOAD_FACTOR:
            self._resize()
        h = hash(key)
        idx = self._index(h)
        node = self.table[idx]
        while node:                                   # walk the chain
            if node.hash == h and node.key == key:    # key exists -> just update
                node.value = value
                return
            node = node.next
        # new key: insert at the FRONT of the chain (O(1), no walk to the end)
        self.table[idx] = Entry(key, value, h, self.table[idx])
        self.size += 1

    def get(self, key: Any, default: Any = None) -> Any:
        h = hash(key)
        node = self.table[self._index(h)]
        while node:
            if node.hash == h and node.key == key:
                return node.value
            node = node.next
        return default

    def remove(self, key: Any) -> None:
        h = hash(key)
        idx = self._index(h)
        prev, node = None, self.table[idx]
        while node:
            if node.hash == h and node.key == key:
                if prev:
                    prev.next = node.next
                else:
                    self.table[idx] = node.next
                self.size -= 1
                return
            prev, node = node, node.next

    def _resize(self) -> None:
        # Rehashing: double the table, re-place every entry with the new modulus
        old = self.table
        self.capacity *= 2
        self.table = [None] * self.capacity
        for head in old:
            node = head
            while node:
                nxt = node.next
                idx = self._index(node.hash)
                node.next = self.table[idx]
                self.table[idx] = node
                node = nxt

    def __len__(self) -> int:
        return self.size


# usage
m = MyHashMap(capacity=4)
for name, roll in [("Ajay", 4), ("Pradeep", 3), ("Upasana", 2), ("Payal", 7)]:
    m.put(name, roll)
m.put("Ajay", 40)                      # update, not a new entry
print(m.get("Ajay"), m.get("Payal"))   # 40 7
print(len(m), m.capacity)              # 4 8   (grew 4 -> 8 when load factor passed 0.75)
m.remove("Payal")
print(m.get("Payal", -1))              # -1

Time complexity: the answer that impresses

Caseget / putWhy
AverageO(1), more precisely O(1 + α)Chains stay short because α is kept ≤ 0.75
WorstO(n)All keys in one chain
Resize stepO(n) onceBut it happens rarely (at 4, 8, 16, 32 … elements), so the amortised cost per insert is still O(1)
Why rehashing is worth it

The teacher once asked his professor why we accept an O(n) rehash. The answer: because the table doubles, the expensive step happens after 64 cheap inserts, then 128, then 256 … Spread over all those inserts, the cost per insert is constant. This is amortised analysis.

Small correction:

A student said a HashMap is fast "because of red-black trees". The teacher rightly said the main reason is the array (direct addressing). For completeness: since Java 8, a single bucket's chain is converted into a red-black tree once it gets long (8+ entries, table size ≥ 64). That limits the worst case for that bucket to O(log n) instead of O(n). Python's dict does not chain at all. It uses open addressing (probing other slots in the same array).

Java vs Python

Java uses key.hashCode() and equals(). In Python they are hash(key) and == (that is, __hash__ and __eq__). Python's % always returns a non-negative result for a positive capacity, even when hash() is negative. In Java you must handle negative hash codes yourself (Java's HashMap uses hash & (n-1)).

Doubt: Why do we need a load factor at all?
→

To know when to rehash. Without it the chains keep growing and performance slowly degrades. Rehashing is not required for correctness. It is an optimisation that keeps the average time complexity low.

Doubt: Does get() ever trigger a resize?
→

No. get only reads and does not change the structure. The check belongs in put, before a new element is added.

Doubt: If the interviewer asks "time complexity of HashMap?"
→

The teacher's joke: if HR asks, say O(1). If an engineer asks, say average O(1) (O(α) with load factor α), worst case O(n), and explain chaining and rehashing. Show depth only when it is asked for.

Key takeaways
  • HashMap = array (O(1) jump) + hash function (key → index) + collision handling (chaining).
  • Load factor = elements / buckets = average chain length. Resize (double and rehash) when it passes 0.75.
  • Average O(1), worst O(n), with resizing amortised to O(1).
  • put = update if the key exists, else insert at the head of the chain. get = walk one chain.

LRU Cache: basic intuition ▶ 2:42:50 – 2:51:14

When the cache is full, throw out the item that was Least Recently Used.

The problem

A cache is fast but expensive, so it has a small, fixed number of slots. If the cache were infinite we would never need LRU. Once all slots are full and a new item arrives, someone must be removed. Which one is decided by the replacement (eviction) policy, a classic hard problem in computer science.

Real-life picture

Five school friends. The one you have not spoken to in five years is the "least recently used". The moment you call them, they become the most recent. Any touch (a get or a put) moves an item to the "most recent" end.

Possible policies: LRU (least recently used), LFU (least frequently used), or your own custom rule. Because the policy is interchangeable, this is the Strategy design pattern.

Walk-through (capacity 4)

OperationOrder (LRU … MRU)Note
put 1, put 2, put 31, 2, 3space available
get 12, 3, 11 was touched → most recent
put 42, 3, 1, 4full now
put 53, 1, 4, 5evict 2 (least recent)
put 61, 4, 5, 6evict 3
put 24, 5, 6, 2evict 1

The order keeps changing, and both get and put must be O(1). A plain array cannot move items cheaply. The next class builds the answer: a doubly linked list for the order plus a hash map for O(1) lookups (see Class 8).

Key takeaways
  • LRU is a cache-eviction policy, needed only because cache space is limited.
  • "Used" means any access: get or put.
  • Requirement: O(1) get and O(1) put while the order keeps changing.
  • Choosing between LRU, LFU and custom policies is a Strategy-pattern decision.