Key-Value Modeling: Keys, TTLs, Caching, and Atomic Operations

A key-value store exposes a simple idea:

key -> value

The simplicity is powerful because the database can optimize direct lookup very well. The trade-off is that application design must make the important access paths obvious in the key.

A key-value model is a good fit when the application usually knows the key before it asks for the value.

Start from the operation

Examples:

session token -> session state
user ID       -> profile cache
rate key      -> request counter
cart ID       -> shopping cart
job ID        -> job state

These are naturally key-oriented.

Less natural:

"find every session whose user lives in Berlin"

That is not a direct key lookup.

A secondary index or another database may be needed.

Key design is data modeling

A key is not merely a string.

It encodes:

  • namespace,
  • identity,
  • sometimes tenant,
  • sometimes time bucket,
  • sometimes version.

Example:

session:8ac17...

Example:

user:42:cart

Example:

rate:user:42:2026-10-01T15

A good key is:

  • deterministic,
  • unambiguous,
  • reasonably compact,
  • stable across application versions.

Namespaces

Prefix related key families:

session:<token>
cart:<user_id>
profile:<user_id>
rate:<user_id>:<window>

This improves:

  • debugging,
  • metrics,
  • bulk maintenance,
  • ownership clarity.

Avoid using a prefix as a substitute for a proper authorization boundary.

Avoid ambiguous concatenation

Bad:

tenant + user
12 + 34 -> 1234
1 + 234 -> 1234

Use separators or structured encoding:

tenant:12:user:34
tenant:1:user:234

Tenant-aware keys

For multi-tenant systems:

tenant:acme:session:...
tenant:beta:session:...

This makes tenant identity explicit.

It does not replace permission checks.

Value shape

A value can be:

  • string,
  • serialized JSON,
  • binary object,
  • specialized data structure.

The value format has a schema even if the database treats it as opaque bytes.

Example JSON value:

{
  "user_id": 42,
  "currency": "EUR",
  "items": [
    {"sku": "DB-101", "qty": 1}
  ]
}

The application still needs versioning and validation.

Redis as a concrete example

Redis provides richer server-side data structures:

  • strings,
  • hashes,
  • sets,
  • sorted sets,
  • lists,
  • streams.

The engine chapter contains a complete Redis note:

Redis

This chapter focuses on modeling choices.

String

Good for:

  • opaque cached JSON,
  • counters,
  • tokens,
  • simple values.

Example:

SET feature:checkout:v2 enabled

Resulting key-value row (the command replies OK):

key value
feature:checkout:v2 enabled

Counter:

INCR article:42:views

Starting key-value row and result:

key value before INCR result value after
article:42:views 9 10 10

Hash

A hash stores fields under one key.

HSET user:42
  name "Alice"
  plan "pro"
  country "DE"

Resulting hash fields:

key field value
user:42 name Alice
user:42 plan pro
user:42 country DE

This can update one field without replacing the entire serialized value.

Set

A set represents unique membership.

SADD team:7:members 42 51 87

Resulting set members (Redis does not promise an order):

key member
team:7:members 42
team:7:members 51
team:7:members 87

Useful for:

  • membership,
  • deduplication,
  • tags.

Sorted set

Members have scores.

ZADD leaderboard 1200 alice
ZADD leaderboard 1500 bob

Resulting sorted-set rows:

key member score
leaderboard alice 1200
leaderboard bob 1500

Useful for:

  • rankings,
  • priority queues,
  • time-ordered scheduling.

TTL

Time-to-live is one of the most important key-value features.

Example:

SET session:abc <value> EX 3600

Resulting key state (the SET command replies OK):

key value expiry
session:abc <value> 3600 seconds

A session expires after one hour.

TTLs are natural for:

  • sessions,
  • verification codes,
  • short-lived caches,
  • rate-limit windows.

TTL is part of business semantics

Ask:

  • what happens after expiration?
  • can the value be rebuilt?
  • does expiration create user-visible behavior?
  • can several related keys expire at different times?

If the only copy of a critical payment state expires automatically, the data model is probably wrong.

Cache-aside

A common caching pattern:

application
    β”‚
    β–Ό
cache lookup
    β”‚
    β”œβ”€β”€ hit -> return
    β”‚
    └── miss
          β”‚
          β–Ό
       primary DB
          β”‚
          β–Ό
       populate cache

The primary database remains authoritative.

Cache key composition

Suppose API response depends on:

  • customer ID,
  • language,
  • currency.

Bad key:

customer:42

It ignores response dimensions.

Better:

customer:42:lang:de:currency:EUR

Cache keys must include every input that changes the result.

Cache invalidation

Suppose:

DB price = 49.90
cache    = 49.90

Database changes to 59.90.

The cache is stale.

Strategies include:

Delete on write

update DB
  β”‚
  β–Ό
delete cache key

Next read reloads.

Short TTL

Staleness is bounded by expiration.

Event-driven invalidation

DB change
  β”‚
  β–Ό
event
  β”‚
  β–Ό
cache invalidator

Versioned key

product:42:v7

Change version to move callers to a new key.

No invalidation strategy is universally correct.

Cache consistency window

Document the tolerated staleness.

Example:

product description -> 5 minutes acceptable
inventory available  -> 5 minutes unacceptable

Different fields may need different cache policies.

Cache stampede

If one popular key expires:

10,000 requests
      β”‚
      β–Ό
all see miss
      β”‚
      β–Ό
10,000 DB queries

Mitigations:

  • request coalescing,
  • jittered TTL,
  • stale-while-revalidate,
  • prewarming,
  • lock/single-flight pattern.

TTL jitter

Instead of every key expiring at exactly 3600 seconds:

3600 Β± random jitter

This spreads refresh work.

Negative caching

An application can cache "not found" briefly.

Example:

user:999 -> missing
TTL 30 sec

Useful when bots repeatedly request nonexistent IDs.

Use short TTLs so newly created data does not remain hidden too long.

Hot keys

A hot key receives disproportionate traffic.

Example:

global:counter

all requests hit one location.

Distributed storage cannot automatically parallelize an operation that truly serializes through one key.

Possible designs:

  • sharded counters,
  • local caches,
  • replicated reads,
  • split state by tenant/time.

The semantics decide what is safe.

Large values

Bad:

one key -> 100 MB object

Costs:

  • network latency,
  • memory,
  • replication,
  • copy/deserialization,
  • eviction impact.

Prefer bounded values.

Large collections

A set/sorted set with millions of members can become an operational hotspot.

Consider partitioning:

leaderboard:2026:region:eu
leaderboard:2026:region:us

if product semantics allow.

Atomic commands

Key-value systems often provide atomic server-side operations.

Example:

INCR inventory:reserved

If inventory:reserved starts at 10, the command returns 11 and stores 11:

key value before result value after
inventory:reserved 10 11 11

is safer than:

GET
application +1
SET

because two clients can race in the read-modify-write sequence.

Compare-and-set

Some systems support conditional writes:

update value only if version == 7

This is optimistic concurrency.

Use it for:

  • locks,
  • lease renewal,
  • versioned state.

Distributed locks

A lock key can coordinate work:

lock:job:123

but distributed locks are easy to misuse.

A robust design needs:

  • unique ownership token,
  • expiration/lease,
  • safe release,
  • fencing/versioning for protected resources,
  • behavior under network pauses.

Do not assume "SET if absent with TTL" solves every coordination problem.

Idempotency key

A useful key-value pattern:

idempotency:<request_id> -> result/status

On retry:

  1. client sends same request ID,
  2. server finds existing result,
  3. server avoids duplicate side effect.

Good for:

  • payment requests,
  • order submission,
  • external webhook processing.

The key must be scoped to the right operation/customer.

Rate limiting

Fixed window:

rate:user:42:2026-10-01T15 -> count

Increment atomically.

Expire after the window.

Trade-off: traffic can burst around boundary.

Other algorithms:

  • sliding window,
  • token bucket,
  • leaky bucket.

Choose from fairness and cost requirements.

Session storage

Key:

session:<random-token>

Value:

{
  "user_id": 42,
  "roles": ["customer"],
  "created_at": "...",
  "csrf_state": "..."
}

Security considerations:

  • token must be unpredictable,
  • TTL should match session policy,
  • sensitive values should be minimized,
  • logout should invalidate/revoke state.

Write-through cache

Application writes through the cache layer to durable storage.

Pros:

  • cache stays populated,
  • consistent write path.

Cons:

  • cache becomes part of critical writes,
  • failure handling becomes more complex.

Write-behind

Cache accepts writes, durable database updates later.

This can reduce apparent latency.

It increases risk:

  • data loss before flush,
  • ordering problems,
  • recovery complexity.

Use only when business semantics tolerate it.

Cache versus source of truth

Ask explicitly:

If every cache key disappears now,
can the system reconstruct correct state?

If yes, it is likely a cache/derived store.

If no, you are using it as primary storage and need a durability/recovery design.

Persistence

Some key-value stores can persist memory state.

Persistence options may include:

  • snapshots,
  • append-only log,
  • replicas.

Persistence changes recovery properties but not automatically business correctness.

See the Redis engine note for concrete details.

Eviction

A cache with bounded memory must choose what happens under pressure.

Potential outcomes:

  • evict old/less-used keys,
  • evict only expiring keys,
  • reject writes.

The application should know what policy applies.

Key cardinality

Track:

  • total keys,
  • keys per namespace,
  • memory per namespace,
  • TTL distribution.

A memory leak can appear as:

session:* key count grows forever

because expiration was forgotten.

Scan versus lookup

Direct lookup:

GET session:abc

Input key-value row:

key value
session:abc {"user_id":42,"role":"reader"}

Result:

key returned value
session:abc {"user_id":42,"role":"reader"}

matches the model.

Scanning every key to find matching values does not.

If the application frequently scans:

all keys where country = DE

add a maintained index/set or use a database designed for that query.

Secondary lookup structures

You can maintain extra sets:

users_by_country:DE -> {42, 87, 101}

But now writes must update:

  • user value,
  • country index.

This is application-managed denormalization.

Define transaction/atomicity behavior.

Sharding

Distributed key-value systems hash or range-partition keys.

Key design affects:

  • load distribution,
  • multi-key operations,
  • locality.

Redis Cluster, for example, uses hash slots.

Related keys can be forced into one slot with hash tags where appropriate.

See the Redis engine note for the concrete behavior.

Multi-key operations

A simple key-value model becomes harder when one operation touches many keys.

Example:

cart:<id>
inventory:<sku>
coupon:<code>
customer:<id>

Ask whether the database supports:

  • atomic multi-key transactions,
  • same-shard requirement,
  • optimistic checks.

Do not assume single-key atomicity extends globally.

Serialization format

Opaque values need a format:

  • JSON,
  • MessagePack,
  • Protobuf,
  • custom binary.

Consider:

  • backward compatibility,
  • forward compatibility,
  • size,
  • debugging,
  • language interoperability.

Schema versioning

Serialized value:

{
  "version": 2,
  "user_id": 42,
  "items": []
}

The application can migrate older values when read.

A cache can often simply invalidate old-version keys instead.

Versioned namespace

Another pattern:

profile:v3:user:42

Deploy new code to use v3.

Old keys expire naturally.

This is simple for derived caches.

Metrics

Monitor:

  • hit ratio,
  • miss ratio,
  • evictions,
  • memory,
  • key count,
  • command latency,
  • hot keys,
  • expired keys,
  • replication lag,
  • rejected connections.

A high hit ratio does not prove value if misses are extremely expensive.

Runnable Redis demo

The engine chapter already includes:

scripts/redis/demo.redis

This NoSQL chapter adds a key-design exercise:

scripts/redis/nosql_key_design.redis

It demonstrates:

  • namespace conventions,
  • TTL,
  • atomic counters,
  • sorted-set ranking,
  • idempotency keys.

Common mistakes

Key does not include all result dimensions

Wrong cached response returned.

No TTL on disposable data

Memory grows forever.

One global hot key

Throughput serializes.

GET-modify-SET for counters

Lost updates occur.

Treating cache as durable truth accidentally

Eviction becomes data loss.

Scanning keyspace for normal queries

Access model is wrong.

Distributed lock without lease/fencing design

Stale owner can corrupt protected resource.

Modeling checklist

  1. What exact key does each request know?
  2. Is value size bounded?
  3. Is this cache or authoritative state?
  4. What TTL should apply?
  5. How is stale data invalidated?
  6. Can a key become hot?
  7. Are updates expressible atomically?
  8. Do operations span multiple keys?
  9. What happens after eviction?
  10. How are retries/idempotency handled?
  11. What serialization/versioning is needed?
  12. Which metrics prove the store is healthy?