Skip to main content
← PROBLEMS / LOW-LEVEL DESIGN
A low-level design interview guide

Sizing Thread Pools
with Little's Law.

By Little's Law (L = lambda * W), when a downstream payment API browns out from 50 ms to 2,500 ms at 400 RPS, required in-flight threads jump from 20 to 1,000—instantly saturating a 200-thread server pool and starving unrelated health/fast endpoints.
Start the walkthrough30 min · Object-oriented design
CONTRACSTATEAPILOW-LEVEL DESIGN
INTERFACES. INVARIANTS. CONCURRENCY.
Difficulty: mediumFocus: littles-law-concurrency-sizing, downstream-brownout-cascading-failureIncludes: Class model & thread safety
Less boilerplate. More understanding.
01 Learn the idea02 Follow the visual03 Know the limits
01 / SCOPE & CONTRACT 3 MIN

Start with the behavior.

Implement a fixed-capacity cache of integer keys and values. Every successful access makes that entry the most recently used (MRU). When a new entry exceeds capacity, remove the least recently used (LRU) entry.

INTERVIEW PROMPT

“I’ll assume a positive capacity and single-threaded use. Both operations need O(1) average time, and storage must stay bounded by capacity.”

LRUCache(int capacity)Create an empty cache. Reject a capacity less than 1 with IllegalArgumentException.
int get(int key)Return the stored value and promote the entry to MRU. Return -1 on a miss; do not change the order.
void put(int key, int value)Update or insert, then promote to MRU. Evict the LRU entry only if the size exceeds capacity.
Keep the assumptions explicit. No TTL, persistence, or concurrent access in the base implementation. The required -1 miss sentinel is ambiguous if stored values can also be -1; a general-purpose API would use an optional result.
02 / DATA STRUCTURES 4 MIN

Lookup is not ordering.

A hash map finds the node.

Map<Integer, Node> maps each key directly to its list node. No traversal to locate an entry.

A linked list tracks recency.

A doubly linked list lets us unlink any known node and insert it at the front with a fixed number of pointer updates.

ApproachLookupMove to MRUWhy / why not
Hash map onlyO(1) avg.Not trackedMissing eviction order
Array / list onlyO(n)O(n)Searching or shifting entries
Hash map + doubly linked listO(1) avg.O(1)Direct node access and unlinking

The invariants to protect

  • Every map entry refers to exactly one live list node, and vice versa.
  • head.next is MRU; tail.prev is LRU when the cache is nonempty.
  • Sentinel nodes are never stored in the map or evicted.
  • After each public operation, map.size() ≤ capacity.
  • Neighbor pointers agree: node.prev.next == node and node.next.prev == node.
03 / OPERATIONS 4 MIN

Follow the pointers.

  1. Get: find, promote, return.

    Look up the node. On a miss, return -1. On a hit, unlink it, insert it after head, and return its value.

  2. Put existing: update in place.

    Change the node’s value and move it to MRU. Size stays the same; no eviction is necessary.

  3. Put new: insert, then enforce capacity.

    Add the new node to both structures. If over capacity, unlink tail.prev and remove its key from the map.

LIVE EXECUTION / TRY THE OPERATIONSCAPACITY 3
3:30MRU
2:20CACHED
1:10LRU
HEAD → MRULRU ← TAIL
Seeded: put(1, 10), put(2, 20), put(3, 30).

Try get(2), then put(4, 40). Key 1 is evicted; key 2 remains cached.

04 / IMPLEMENTATION 7 MIN

Small helpers. Fewer edge cases.

Dummy head and tail nodes make insertion and removal uniform, even when the cache has only one entry. Store the key in each node so eviction can remove its map entry without a search.

JAVA / SINGLE-THREADED REFERENCE
import java.util.HashMap;
import java.util.Map;

public final class LRUCache {
    private static final class Node {
        final int key;
        int value;
        Node prev, next;

        Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    private final int capacity;
    private final Map<Integer, Node> map = new HashMap<>();
    private final Node head = new Node(0, 0);
    private final Node tail = new Node(0, 0);

    public LRUCache(int capacity) {
        if (capacity < 1) {
            throw new IllegalArgumentException(
                "Capacity must be positive"
            );
        }
        this.capacity = capacity;
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        Node node = map.get(key);
        if (node == null) return -1;

        moveToFront(node);
        return node.value;
    }

    public void put(int key, int value) {
        Node node = map.get(key);
        if (node != null) {
            node.value = value;
            moveToFront(node);
            return;
        }

        Node added = new Node(key, value);
        map.put(key, added);
        insertAtFront(added);

        if (map.size() > capacity) {
            Node victim = tail.prev;
            unlink(victim);
            map.remove(victim.key);
        }
    }

    private void moveToFront(Node node) {
        unlink(node);
        insertAtFront(node);
    }

    private void unlink(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
        node.prev = null;
        node.next = null;
    }

    private void insertAtFront(Node node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
}

This implementation is intentionally not thread-safe. Synchronizing only the map would not protect the list or the cross-structure invariants.

05 / TESTS & COMPLEXITY 4 MIN

Prove the behavior, not just the code.

Operation / resourceComplexityReason
get / putO(1) averageHash map operation + fixed pointer updates
EvictionO(1) averageDirect LRU pointer + map removal
SpaceO(capacity)One map entry and one node per cached key

“Average” matters: hash map operations are not a strict worst-case O(1) guarantee. No throughput or nanosecond latency claims are needed to establish this design’s complexity.

A minimal test checklist

  • Empty cache: get returns -1 without changing state.
  • Capacity one: the next distinct insertion evicts the previous entry.
  • Existing key: value updates, size stays fixed, and the key becomes MRU.
  • Get hit: the accessed key survives the next eviction.
  • Get miss: the eviction order remains unchanged.
  • Repeated eviction: removed keys are absent from both structures.
  • Invalid capacity: construction throws immediately.
CANONICAL TRACE / CAPACITY 2
put(1, 10)  → [1:10]
put(2, 20)  → [2:20, 1:10]
get(1)      → 10       [1:10, 2:20]
put(3, 30)  → evict 2  [3:30, 1:10]
get(2)      → -1       [3:30, 1:10]
put(1, 99)  → update   [1:99, 3:30]

Order shown: MRU → LRU
CHECK YOUR UNDERSTANDING

Why isn’t get() a read-only operation?

06 / TRADE-OFFS 3 MIN

Extend only when asked.

The base solution is complete. Keep follow-up discussion local to the cache’s contract and implementation, rather than introducing a distributed architecture.

What if concurrent access is required?

Start with one lock covering each complete get and put operation, including map and list mutations. A get hit also writes recency state, so a read lock is not enough. This preserves exact global LRU but serializes access. Sharding can reduce contention, but independent shard eviction is not exact global LRU.

Could I use LinkedHashMap instead?

Yes. An access-order LinkedHashMap can track recency, and removeEldestEntry can enforce capacity. It reduces custom code, but in a data-structure interview, first demonstrate the node and pointer mechanics. It is not thread-safe by default.

What about expiration or a byte limit?

Those change the contract. TTL requires expiration tracking and cleanup; a byte limit requires size accounting and may evict multiple entries per insertion. Do not carry the base O(1) put guarantee over without reanalyzing that extra work.

What is the main weakness of LRU?

A scan of many one-time keys can evict frequently useful entries. Alternatives such as LFU or segmented policies address different workloads, but they are separate requirements—not necessary additions to this problem.

THE 30-SECOND SUMMARY

“A hash map finds each node; a doubly linked list maintains recency. Hits and writes move entries to the front. Overflow removes the tail entry from both structures. The result is O(1) average get and put, using O(capacity) space.”

Recommended Practice Flow