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
- Course roadmap: the LLD problems to practise
- LLD of HashMap
- LRU Cache: basic intuition
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.
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
| # | Step | What you actually do | Parking-lot example |
|---|---|---|---|
| 1 | Clarify requirements | Ask 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. |
| 2 | Identify core entities, enums, interfaces | Only 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. |
| 3 | Define relationships | Say 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. |
| 4 | Start coding | Before 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." |
| 5 | Discuss edge cases & extensibility | Concurrency (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
- Functional requirement (FR): a feature the system must have. Without it, the system is wrong. Example: "park a vehicle", "issue a ticket".
- Non-functional requirement (NFR): a quality. Without it the feature still works, but badly: slow, unsafe under concurrency, hard to extend. Examples: thread safety, performance, clean code, SOLID, extensibility, loose coupling.
Step 2 in detail: class or enum?
- Entity = a class = state (fields) + behaviour (methods). User, Order, ParkingLot (it can
park_vehicle()andunpark_vehicle()). - Enum = a fixed set of readable names with no real behaviour. Use enums for types and states:
VehicleType.CAR / TRUCK / MOTORCYCLE / VAN,SlotType.SMALL / MEDIUM / LARGE / HANDICAPPED, vending-machine statesIDLE / DISPENSING / RETURNING_CHANGE. Use them instead of magic numbers like 0 = car, 1 = truck. - Interface = a contract for behaviour that can vary (payment method, pricing strategy).
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
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 CARR-E-R-C-E: Requirements (FR + NFR) → Entities/enums/interfaces → Relationships → Code (name your patterns first) → Edge cases & extensions.
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.
→
A class: something with state (fields) and behaviour (methods). Things that are only "types" or "states" become enums.
→
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.)
- 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.
| # | Problem | What it teaches |
|---|---|---|
| 1 | Parking lot | The classic. Entities, enums, relationships. (One student failed it at KPMG.) |
| 2 | Elevator system | State and scheduling |
| 3 | Library management | CRUD-style design with central management of users and items. Also stands in for "banking-like" systems. |
| 4 | Ride sharing (Uber/Ola) | Strategy, state machine, real-time matching, concurrency |
| 5 | Hotel booking | Concurrency and locking on shared inventory. The teacher said concurrency is "an extra 20 marks". |
| 6 | Chess (plus tic-tac-toe and snake & ladder, already done) | Board-game modelling |
| 7 | ATM machine | State pattern |
| 8 | Food ordering (Swiggy/Zomato) | A frequently asked e-commerce-style flow |
| 9 | Shopping cart / e-commerce | A favourite at Amazon |
| 10 | Notification system | Executor service, thread pools |
| 11 | Rate limiter | Simple LLD, but the algorithms matter |
| 12 | Load balancer | Balancing algorithms (consistent hashing may be covered here) |
| 13 | Thread pool | Hard 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.
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.
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
- Hash function: a formula that takes a key and returns an index in
0 … table_size - 1. Typicallyindex = hash(key) % capacity. A good hash function is deterministic (same input, same output), spreads keys evenly, and is fast. - Collision: with more keys than slots, two keys will sometimes land on the same index. With 4 slots and 10 keys, this is guaranteed.
- 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
Entryreferences, much like an adjacency list in graphs. - Worst case: every key hashes to the same bucket. That gives one long chain, and lookup becomes O(n).
- 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. - 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.
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- put: check for a resize first, because only
putadds elements. Then compute the index and walk the chain. If the key exists, update its value. Otherwise insert at the front of the chain, which is O(1) with no walk to the end. That is what the teacher's Java code did. - get: compute the index and walk the chain. Return the value, or a default (Java returns
null; some versions return -1). - Cached hash in Entry: an optimisation, so resizing does not recompute every key's hash.
Time complexity: the answer that impresses
| Case | get / put | Why |
|---|---|---|
| Average | O(1), more precisely O(1 + α) | Chains stay short because α is kept ≤ 0.75 |
| Worst | O(n) | All keys in one chain |
| Resize step | O(n) once | But it happens rarely (at 4, 8, 16, 32 … elements), so the amortised cost per insert is still O(1) |
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.
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 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)).
→
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.
→
No. get only reads and does not change the structure. The check belongs in put, before a new element is added.
→
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.
- 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.
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)
| Operation | Order (LRU … MRU) | Note |
|---|---|---|
| put 1, put 2, put 3 | 1, 2, 3 | space available |
| get 1 | 2, 3, 1 | 1 was touched → most recent |
| put 4 | 2, 3, 1, 4 | full now |
| put 5 | 3, 1, 4, 5 | evict 2 (least recent) |
| put 6 | 1, 4, 5, 6 | evict 3 |
| put 2 | 4, 5, 6, 2 | evict 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).
- 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.