Concrete scenario
Sessions need lookup by token, while a catalog needs predicates on nested product attributes.
Mechanics
A minimal key-value store exposes opaque bytes by key, but products vary: Redis has server-side structured types and DynamoDB supports attributes and secondary indexes. Document stores expose fields and indexes inside records, paying parsing and index-maintenance costs.
Failure mode
Manually maintained secondary structures can diverge after a partial write. Too many document indexes amplify writes, and large documents can create contention or transfer waste.
Trade-off
Key-based access is simple and predictable; document queries improve flexibility. Select by required operations rather than assuming every key-value value is opaque or every document store provides relational constraints.
Worked example: sessions vs product catalog
Sessions: 10M active sessions, each about 1 KB, with 50k reads/s and 5k writes/s. Access is always get(token) and set(token, blob, ttl). That makes it a key-value workload. In Redis, 10M x about 1.1 KB is about 11 GB of RAM, with sub-millisecond reads and native TTL. In DynamoDB, 50k eventually consistent reads/s of items up to 4 KB cost 25k RCUs, with DynamoDB TTL for expiry. No secondary index is needed.
Catalog: 5M products with nested attributes (size, colour, specs). Queries include "category = shoes AND brand = X AND price < 100 sorted by rating". That is a document workload. MongoDB with a compound index {category: 1, brand: 1, price: 1} serves it, and partial updates with the set operator (for example setting only stock.warehouse3 to 12) avoid rewriting the whole document. Putting this in a pure KV store means building and maintaining every index yourself.
Comparison
| Aspect | Key-value (Redis, Memcached, DynamoDB core) | Document (MongoDB, Couchbase, Firestore) |
|---|---|---|
| Lookup | By key (plus sort key in DynamoDB) | By key or any indexed field, including nested fields |
| Update | Usually replace the whole value (Redis types and DynamoDB UpdateExpression are exceptions) | Field-level update operators (set, inc, push) |
| Secondary indexes | None (Redis, Memcached) or limited (DynamoDB GSI/LSI, eventually consistent GSIs) | Many, including multikey (array) and text |
| Write cost | Lowest: one key | Grows with the number of indexes |
| Latency profile | Very predictable | Depends on query shape and index use |
| Typical fit | Sessions, caches, carts, feature flags, idempotency keys | Catalogs, CMS content, user profiles, event payloads |
Modeling rules for documents
- Embed what is read together and bounded (an order and its line items).
- Reference what is unbounded or shared (a post's comments, a product shared by many orders).
- Keep hot, frequently updated fields (view counters) out of large documents, or in a separate KV counter.
Failure modes
- GSI lag in DynamoDB: a read from a global secondary index right after a write may not see it.
- Index explosion: 15 indexes on a document collection can make each insert cost 16 B-tree updates.
- Hot documents: a single "global counters" document serializes all writers on one lock.