AAI Agentic Handbook
Problem 126 · Practical builds

In-Memory Key-Value Store: TTL, Transactions, Snapshots

MediumPractical multi-stage build
01Problem

Build an in-memory key-value store in four stages, each extending the last. The final version supports get/set/delete, per-key TTL against an injected clock, nested BEGIN/ROLLBACK/COMMIT, and snapshot/restore.

Stages — finish each before reading the next
1Core store

Implement set(key, value), get(key, default) and delete(key) -> bool (True if the key existed).

2TTL

set accepts an optional ttl in seconds. Expired keys read as missing; ttl(key) returns remaining seconds, None for no expiry, and raises KeyError for a missing key. Time comes from an injected clock so tests are deterministic.

3Nested transactions

Add begin(), rollback() and commit(). Transactions nest: an inner commit merges into its parent only; rollback discards the innermost layer. Deletes inside a transaction must shadow committed values. Commit/rollback with no open transaction raises.

4Snapshot / restore

snapshot() returns a deep copy of live committed state; restore(snap) replaces the store, dropping entries that expired since. Both refuse to run while a transaction is open.

02Key insight

Model each transaction as a write-overlay dict on a stack, with a sentinel for deletes. Reads walk the stack top-down then fall through to committed data, commit is a merge into the parent layer, rollback is a pop. Store absolute expiry times, not TTLs, so overlays and snapshots stay correct as the clock moves.

03Solution

Try it first.

Timeget/set/delete O(d) for transaction depth d; commit O(keys in layer); snapshot O(n)
SpaceO(n + writes in open layers)
tests.py · 13 passing
tests.pypython51 lines
t = [0.0]
kv = KVStore(clock=lambda: t[0])
kv.set("a", 1)
assert kv.get("a") == 1 and kv.get("missing") is None
assert kv.delete("a") is True and kv.delete("a") is False
# TTL
kv.set("s", "tok", ttl=10)
t[0] = 9.0
assert kv.get("s") == "tok" and kv.ttl("s") == 1.0
t[0] = 10.0
assert kv.get("s") is None
try:
    kv.ttl("s"); assert False
except KeyError:
    pass
# nested transactions
kv.set("x", 1)
kv.begin(); kv.set("x", 2)
kv.begin(); kv.delete("x")
assert kv.get("x") is None
kv.rollback()
assert kv.get("x") == 2
kv.begin(); kv.set("y", 9); kv.commit()   # inner commit merges into outer only
assert kv.get("y") == 9
kv.rollback()                              # outer rollback discards both
assert kv.get("x") == 1 and kv.get("y") is None
kv.begin(); kv.set("x", 5); kv.commit()
assert kv.get("x") == 5
try:
    kv.commit(); assert False
except RuntimeError:
    pass
# TTL inside a transaction is evaluated at read time
kv.begin(); kv.set("e", 1, ttl=1); t[0] = 11.5
assert kv.get("e") is None
kv.commit()
# snapshot / restore, deep-copied and expiry-aware
kv.set("list", [1, 2]); kv.set("short", 1, ttl=2)
snap = kv.snapshot()
kv.get("list").append(3); kv.delete("x")
assert kv.get("list") == [1, 2, 3]
kv.restore(snap)
assert kv.get("list") == [1, 2] and kv.get("x") == 5 and kv.get("short") == 1
t[0] = 20.0
kv.restore(snap)
assert kv.get("short") is None and kv.get("x") == 5
kv.begin()
try:
    kv.snapshot(); assert False
except RuntimeError:
    pass
04Staff-level follow-up

How would you make this durable and concurrent? Append-only log of committed writes plus periodic snapshots (Redis AOF + RDB); for concurrency use MVCC (each transaction reads a version, conflicts detected at commit) rather than one global lock; expire actively with a sampled sweep, not only lazily on read.