skip to content

What single behavior separates an insertion-ordered map from an access-ordered LRU cache?

level: middleimportance: should knowfreq 52%

answer

  1. The difference lives on the read path
  2. Does a lookup move anything?
  3. One order is a chronology of arrivals
  4. The other is a live recency ranking
  5. Neither of them is sorted by key

basics

~20 s

Whether a successful lookup relinks its node. If reads leave the list untouched, iteration follows insertion order; if each read moves its node to the recent end, the order becomes recency and the far end is the eviction victim.

solid answer

~50 s

Both are the same composition — a hash map whose values are nodes of a doubly linked list — and they differ in exactly one line of the read path. In insertion order, a lookup returns the payload and touches nothing, so the list preserves the sequence in which keys were first added and iteration is stable and reproducible. In access order, a successful lookup also unlinks the node and splices it back at the recent end, so the list is a live recency ranking and the stale end is precisely the LRU eviction candidate. The consequence people miss is that access order makes **reads mutate the structure**: two readers touching the cache concurrently are two writers, and iterating while reading reorders the thing being iterated. Insertion order costs nothing on reads and buys determinism instead of eviction ordering.

go deeper

for a junior

Be able to say that one order records when keys arrived and the other records when they were last used, and that neither has anything to do with sorting keys.

for a middle

Explain the switch mechanically: which operation relinks the node, and why evicting from the stale end of an arrival-ordered container gives first-in-first-out rather than recency.

for a senior

Show you have felt the cost — reads that mutate shared state, iteration that reorders itself, and downstream code that quietly starts depending on a deterministic iteration order.

for a principal

Own the API consequence: once iteration order is observable it is a contract you cannot withdraw, so decide deliberately whether ordering is a guarantee you publish or an internal detail of a cache.

## One composition, two contracts Thread a doubly linked list through the entries of a hash map and you get a container with a defined iteration order. Which order you get is a policy decision on the read path, and it is genuinely a one-line switch. **Insertion order:** a node is spliced in when the key is first added, and never moved again. A later write that overwrites an existing key updates the payload in place and leaves the position alone — the entry keeps its original seniority. Reads are pure. Iteration therefore replays the exact sequence of first insertions. **Access order:** the same splice happens on first insert, but *every successful lookup* also unlinks the node and re-splices it at the recent end, and so does every overwrite. The list is now a recency ranking, continuously maintained. The far end is the least recently used entry, which is what makes eviction O(1). Neither is *sorted* order. This is the most common confusion in the answer: neither variant compares keys, and neither can answer "give me the smallest key" or a range query — those need an ordered tree structure with O(log n) operations. The order here is an event log (when it arrived, or when it was last touched), not a comparison result. ## What insertion order actually buys Determinism. Consider a photo service that serialises the current state of a rendering pipeline — a per-request record of which size variants were requested, in the order the client asked for them — into a debug dump. With a plain hash map, two runs over identical input can emit those entries in different arrangements, which makes diffs noisy and makes a byte-for-byte comparison of two dumps useless. With insertion order, the dump is reproducible, and a reviewer can read it as a chronology. That determinism is not free of consequence, though: it means iteration order is now part of your observable behaviour, and someone downstream will start depending on it. If you later swap the container for a plain hash map, you break them silently. ## What access order costs Three things, all of which an interviewer likes to hear unprompted. 1. **Reads become writes.** A lookup performs pointer writes on shared structure. A design that assumed "many readers are harmless" is no longer true — a shared access-ordered cache needs the same exclusion around a read as around a write, and the read path now dirties memory that other cores may hold. 2. **Iteration is unstable under reads.** Walking the container while other code reads entries means the traversal is reordering itself underneath you. Even in a single-threaded loop, reading a value through the cache during iteration moves the node you are standing on. 3. **A small constant cost on every hit.** Unlink plus splice is roughly six pointer writes on a path that would otherwise be a pure lookup. That is invisible next to rendering a thumbnail and very visible in front of a nanosecond-scale in-memory value. ## The failure mode this aims at The wrong answer is "an order-preserving map already gives me an LRU cache — I just drop the last entry when it is full." It does not. In insertion order, the entry at the stale end is the one that arrived *first*, not the one that was *used* least recently. A key inserted at start-up and hammered a million times per second is still sitting at the eviction end, and you will evict your hottest entry while a key inserted a second ago and never touched again survives. That is an anti-LRU cache: it approximates first-in-first-out, and under a workload with a hot working set it can be dramatically worse than random eviction. The fix is the relink on read, and only the relink on read. ## A note on how different runtimes expose this The degrees of freedom here are visible in the choices mainstream ecosystems made. Java ships an explicit order-preserving map variant alongside its unordered one, with an opt-in flag that switches insertion order to access order; Python made insertion order the guaranteed behaviour of its ordinary dictionary; Go deliberately randomises map iteration order so that no program can accidentally depend on it. Three defensible calls on the same question — the point is that iteration order is a *policy*, not a property of hashing, and you should know which policy the container in front of you implements rather than assuming. ## Choosing between them Ask what you need the order *for*. If you need reproducible output, replayable configuration, or a stable dump — insertion order, and keep reads free. If you need bounded memory with a recency-based victim — access order, and accept that reads mutate. If you need range queries or sorted iteration, neither: you want a comparison-ordered structure and its O(log n) costs. And if you need no order at all, take the plain hash map and skip the per-entry pointer overhead entirely.

  • If I evict from the stale end of an insertion-ordered map, what policy have I actually built?
    First-in-first-out, not least-recently-used. The stale end holds the key that arrived earliest, regardless of how often it has been read since. A hot entry inserted at start-up is your next victim while a cold entry added moments ago survives. Under a workload with a stable hot set that behaves close to anti-LRU, and it can be worse than evicting at random.
  • In access order, does overwriting an existing key's value count as a use?
    It should. A write is a touch: the entry is demonstrably live, so leaving it in place lets a constantly-rewritten key drift to the eviction end and get dropped. Treat the overwrite path as "update the payload, then relink to the recent end" — the same relink the read path performs.
  • Why is it risky to hand an access-ordered cache to callers as a general-purpose map?
    Because their reads silently mutate it. Iterating it, snapshotting it, or reading it from two places at once no longer behaves like a map — the traversal reorders itself, and concurrent readers are concurrent writers. Expose a narrow get/put surface instead, and keep the ordering behaviour an implementation detail of the cache.

saying these in an interview costs you the question

  • Thinks an insertion-ordered map is already an LRU cache
  • Says access order means sorted by key
  • Claims reads never modify an access-ordered container
  • Forgets that overwriting a key counts as a use
  • Expects range or floor queries from either ordering

context