Low-Level Design (Object-Oriented Design) Module 16 – Case studies: infrastructure components
Low-level design: key-value store with TTL
Design an in-memory key-value store: lazy and sampled expiry, nested transactions as change sets with tombstones, and commits that catch lost updates.
What you will learn
- Implement get, set and delete with expiry, both lazy and periodic
- Add nested transactions with begin, commit and rollback
- Design operations that stay thread-safe and snapshot-friendly
- Catch lost updates between sessions with versions
Before you start
What you’ll learn · Before you start
What you will learn
- Implement get, set and delete with expiry, both lazy and periodic
- Add nested transactions with begin, commit and rollback
- Design operations that stay thread-safe and snapshot-friendly
- Catch lost updates between sessions with versions
Before you start
On this page
An in-memory key-value store with a time to live (TTL) keeps values under keys inside one process, forgets each key when its time is up, and lets a client group changes into transactions. In a low-level design (LLD) round it comes down to four classes. A Store maps each key to an Entry: the value, an optional deadline and a version number, behind one lock. Expiry has two halves: an operation that finds a key past its deadline deletes it, and a periodic cycle deletes expired keys that nobody reads. A Session runs one client’s commands, and its nested transactions are a stack of change sets: a read looks in the innermost set first, a delete writes a tombstone, ROLLBACK drops the top set and COMMIT merges it into the one below. Time comes from a Clock the store is given.
The prompt, the requirements and the first design
The prompt: “Design an in-memory key-value store for an app’s login sessions and one-time codes, with SET, GET, DEL, EXPIRE and TTL, keys that expire, and nested transactions with BEGIN, ROLLBACK and COMMIT.” Five questions shape it:
- Values and units? Strings, and times to live in milliseconds.
- What does a read inside a transaction see? Its own writes first; other clients see them after COMMIT.
- COMMIT inside a nested transaction? It merges outwards; only the outermost reaches the store.
- Many clients? Yes, each with its own session.
- Memory? Expired keys must not pile up unread.
The answers become numbered requirements:
- K1 SET stores a value and replaces any old value and its expiry.
- K2 GET returns the value, or nothing for a missing or expired key.
- K3 DEL removes a key; EXPIRE gives a key a new time to live, and 0 or less deletes it.
- K4 TTL answers the milliseconds left, -1 for a key without expiry and -2 for a missing key.
- K5 An expired key is never returned, and its memory comes back even if nobody reads it.
- T1 to T4 Transactions nest; a read sees the innermost change first; ROLLBACK discards the innermost transaction and COMMIT merges it outwards; the outermost COMMIT applies everything at once, or nothing.
- N1 Time comes from a clock the store is given; N2 every operation and every commit is atomic.
The classes of the key-value store
Text description of the diagram
The diagram shows five classes in UML notation, each box naming a class, then its attributes and, below a line, its operations. A filled diamond marks the owner of a composition.
- Session holds stack, a list of change sets, and read, a map from each key the open transaction read to that key's version in the store. Its operations are execute(line), which returns the reply text, begin(), rollback(), which returns a boolean, and commit(), which returns a text. It owns zero or more ChangeSets, and an arrow says it reads from and commits to the Store.
- ChangeSet holds changes, a map from each key to a value with its deadline, or to a tombstone.
- Store holds data, a map from each key to an Entry, volatile, the keys that have a deadline, and one lock. Its operations are set(key, value, ttl), get(key), expire(key, ttl), expire_cycle(), which returns how many keys it deleted, and apply(changes, read), which returns whether a commit was applied. It owns zero or more Entries and reads a Clock.
- Entry has a value, a deadline that may be None, and a version.
- Clock is an interface with one operation, now_ms(), which returns whole milliseconds.
Expiring keys only when someone reads them looks enough, until the keys that nobody reads again are counted:
"""Why a store cannot expire keys only when they are read.
A store keeps 10,000 login sessions, each with a time to live of 30 minutes, and removes an expired entry only when a
read finds it. An hour later, 200 of those users come back.
"""
MINUTE = 60_000
class LazyOnlyStore:
def __init__(self):
self.entries = {} # key -> (value, deadline in ms)
def set(self, key, value, ttl_ms, now):
self.entries[key] = (value, now + ttl_ms)
def get(self, key, now):
entry = self.entries.get(key)
if entry is not None and entry[1] <= now:
del self.entries[key] # expired: removed, but only because someone asked
return None
return None if entry is None else entry[0]
store = LazyOnlyStore()
for user in range(10_000):
store.set(f"session:{user}", "token", ttl_ms=30 * MINUTE, now=0)
now = 60 * MINUTE
expired = sum(1 for _, deadline in store.entries.values() if deadline <= now)
print(f"after an hour: {len(store.entries):,} entries held, {expired:,} of them expired")
answers = [store.get(f"session:{user}", now) for user in range(0, 10_000, 50)]
print(f"200 reads later: every read answered {set(answers)}; {len(store.entries):,} entries held, all expired") Output
after an hour: 10,000 entries held, 10,000 of them expired
200 reads later: every read answered {None}; 9,800 entries held, all expired
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lazy_only.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
An hour later all 10,000 entries had expired, and 200 reads removed only the 200 they touched. The design names these options:
- Lazy expiry: an operation that finds a key past its deadline deletes it and answers as if it were missing.
- Sampling: a periodic cycle tests a few random keys that have a deadline and deletes the expired ones.
- A deadline heap: a min-heap of deadlines pops every key whose time has come, exactly on time.
- Tombstone: a marker in a change set that says “deleted here”, so an older value below stays hidden.
- Change sets: each open transaction keeps only what it wrote, and reads fall through to the store.
- Copy at BEGIN: a transaction starts with its own copy of the whole store.
- Version check: a commit fails when a key it read has changed since.
Redis documents the same two halves for its EXPIRE command, a passive way on access and an active way that tests a few random keys with an expiry.
Not affiliated
Based on public information about Redis. MySmartCoPilot is not affiliated with Redis, and Redis’s real expiry and transactions may differ from what this lesson describes.
The deep dives that decide whether this design holds:
- How do the Store, its Entries and a Session look in code, and what does one session print?
- What do lazy expiry, sampling and a heap cost in memory and work, measured?
- Why do deletes inside a transaction need tombstones, and what must a rollback restore?
- What happens when two sessions change the same key, and how does a commit notice?
- How do locks, shards and snapshots fit, and what changes in five other languages?
An answer is judged on the requirements, the split between store, sessions and clock, the expiry policy and its numbers, transactions that keep their promises, and concurrency.
The store and a session in code Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Expiry three ways, measured
- Nested transactions: change sets, tombstones and what a rollback restores
- Two sessions, one key: the lost update and the version check
- Locks, shards and snapshots
- One session, six languages
- Practise: COUNT by value in O(1), inside transactions
- COUNT by value in O(1), inside nested transactions
- Designs this one rejects
- What each level is expected to cover
- A 45-minute self-mock
- Check yourself
- Interview questions
- Why does a key-value store with TTLs need both lazy and active expiry?
- Inside a transaction a client deletes a key, then reads it and gets the old value back, and after COMMIT the key still exists. Explain the bug and fix it, and say what ROLLBACK must restore when a transaction changed a key's expiry.
- Your store holds 2 million session keys with 30-minute TTLs, and memory keeps growing although most users never come back. Expiry is lazy only. Design active expiry: what it examines, how often, what it costs, and how you choose between sampling and a heap of deadlines.
- Two clients each run BEGIN, GET views:42, SET views:42 to that value plus one, and COMMIT, at the same moment, and the counter rises by one instead of two. Explain why, and change the commit so this cannot happen without locking the key for the whole transaction.
- Key takeaways
1 diagram · 5 runnable examples (Scenario.java, expiry.py, kv.py, run_six.py, scenario.cpp, scenario.go, scenario.mts, scenario.py, scenario.rs, session.py, sessions.py, tx_drill.py) · 1 exercise with sample tests · 3 interview answers · about 26 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; nothing renews unless you choose to. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Read a complete sample first: Low-level design: parking lot (the model)
References
- EXPIRE: how keys expire (passive and active), and what clears a timeout (Redis)
- TTL: remaining time to live, -1 and -2 (Redis)
- Transactions: MULTI, EXEC, DISCARD and WATCH (Redis)
- heapq: Heap queue algorithm (Python Software Foundation)
- threading: Lock objects (Python Software Foundation)
- dataclasses: frozen instances (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress