Lesson 02 · The minimalist

Key-value: the fastest thing that can possibly work

Delete everything from the relational model: tables, columns, joins, constraints, the query language itself: until only one idea remains: a giant dictionary. PUT(key, value), GET(key), DELETE(key). The value is an opaque blob; the store neither knows nor cares what's inside. What's left when you delete everything? Speed. A hash of the key names the exact location of the value: no planner, no B-tree descent, no parsing: which is why Redis answers in microseconds and DynamoDB promises single-digit milliseconds at any scale.

The whole engine, in one lab

PUT and GET against a hash-slot store

Eight slots, hash function h(key) mod 8. Store some session data and fetch it back: watch which slot lights up and count the steps.

operation cost
:
keys stored
0

Notice what the lab never needed: a schema. Sessions, config, whatever: all just keys. This shape happens to be exactly the shape of an enormous class of real workloads: session stores, shopping carts, feature flags, rate-limit counters, user preferences, shortened URLs. In every one, the application always knows the full key ("the session id is in the cookie") and always wants the whole value. When the access pattern is that clean, everything a relational engine does per-query is pure overhead.

The killer app: caching

Cache-aside with a TTL

Product pages backed by a slow database (80ms). The cache answers in 1ms but entries expire after a TTL (here, ~2 fetches' worth of time, simulated). Fetch repeatedly and watch the hit rate and the latency you'd actually feel.

cache
empty
hits / misses
0 / 0
last latency
:
DB queries saved
0

The pattern you ran is cache-aside: on miss, read the database, then PUT the result with a TTL; on hit, skip the database entirely. The TTL is doing quiet philosophical work: it's an eventual consistency dial (replication course, lesson 3's vocabulary): a 60-second TTL means "I accept up-to-60-seconds-stale product pages in exchange for 80× lower latency and a database that survives the traffic." Almost every high-traffic system you've used is a relational core wearing a key-value cache as armor.

The wall

Now query by value

The store holds five user records. Product asks: "find all users older than 30." The only tools that exist: GET by key. Try it.

user:101{name:"Mia", age:34}
user:102{name:"Rui", age:28}
user:103{name:"Sam", age:41}
user:104{name:"Ana", age:25}
user:105{name:"Ben", age:37}

The blob's opacity: the exact property that made writes and reads trivially fast: makes the question unanswerable except by fetching every key and filtering in application code. The workarounds all amount to maintaining your own indexes by hand: keep a second key age-over-30 → [101,103,105] and update it on every write (now you own the consistency between them: no transactions to help), or use a store that's grown past pure key-value (DynamoDB's secondary indexes, Redis's sorted sets: each a deliberate step back toward structure). The honest rule: key-value fits when you will only ever ask for things by their name. The moment "find all X where..." enters the roadmap, you're either leaving the model or rebuilding Lesson 1 inside your app.

Species notes: Redis: in-memory, microsecond latency, rich value types (lists, sets, sorted sets, streams) that stretch the model while keeping key-addressing; persistence optional, often used as cache/queue/lock-service. Memcached: the pure minimal cache. DynamoDB & Riak: durable, partitioned key-value at planetary scale (their rings are Lesson 3 of the distributed systems course; their quorums, Lesson 3 of the replication course). etcd & Consul: small, consensus-backed key-value stores where the point isn't speed but Raft-grade correctness for configuration and locks.

Check yourself

1. Why can a key-value store promise O(1) lookups while the relational B-tree from Lesson 1 is O(log n)?

A hash table trades order for speed: h(key) says exactly where to look, but neighboring keys land in unrelated slots. B-trees keep keys sorted precisely so range scans work. This one trade: order vs direct addressing: explains half the differences between database species (and why DynamoDB added a sort key within each partition: hash to find the partition, order inside it).

2. A product page cache has TTL 300s. Marketing changes a price; some users see the old price for up to 5 minutes and file a bug. What's the correct diagnosis?

Cache-aside has no push channel; staleness ≤ TTL is the contract, not a defect. Explicit invalidation tightens it but introduces the famous hard problem (the DELETE can race the next read, be lost, or miss a key variant): hence the industry proverb about cache invalidation being one of the two hard things. Decide staleness tolerance per data type: prices maybe 10s, avatars a day.

3. A team stores orders in a key-value store as order:{id} → blob. A new requirement arrives: "show all orders for customer X." What is the realistic set of options?

Full iteration is O(everything) per request: the lab's wall. Key renaming only helps stores with ordered key scans (and only for prefix patterns). The real choice is the honest one: hand-maintained inverse mappings (with all their race conditions), a native secondary-index feature (DynamoDB GSIs: which are themselves eventually-consistent hand-rolled indexes, managed for you), or a model change. "We'll never query by value" is a bet; requirement drift is how it's lost.