Durable KV Store

Overview

Implement a single-machine key-value store that survives crashes. put and delete change an in-memory map, but the store must recover its state after a restart, so every mutation is first appended to a write-ahead log (WAL) on disk. The in-memory map needs no fancy optimization; the interesting parts are durability, thread safety, and keeping the WAL from growing forever.

The key ordering rule is: write the WAL and fsync it before updating the in-memory map. If you update memory first and crash before the log is on disk, recovery loses that mutation and the state is inconsistent.

Interfaces

class DurableKVStore:
    def __init__(self, wal_path):
        self._map = {}
        self._lock = Lock()
        self._wal = open(wal_path, "a+")
        self.recover()                    # rebuild state from disk on startup

    def put(self, key, value):
        with self._lock:
            self._append_wal({"op": "PUT", "key": key, "value": value})
            self._map[key] = value

    def get(self, key):
        with self._lock:
            return self._map.get(key)

    def delete(self, key):
        with self._lock:
            self._append_wal({"op": "DELETE", "key": key})
            self._map.pop(key, None)

    def _append_wal(self, record):
        self._wal.write(encode(record) + "\n")
        self._wal.flush()                 # hand buffer to the OS
        os.fsync(self._wal.fileno())      # force it durably to disk

A single global lock around both the WAL append and the map update is the simplest correct version: thread-safe, durable, and it preserves operation order. Its costs are that all reads and writes serialize and every write pays an fsync.

Inputs and outputs

  • Input: put(key, value), get(key), and delete(key) from one or many threads.
  • Output: after a crash and restart, the store recovers exactly the mutations that were acknowledged before the crash.

Requirements

  • A mutation is durable before its call returns: WAL append, then fsync, then the memory update.
  • Concurrent access is safe — readers never see a half-applied write.
  • Recovery rebuilds the map by replaying the log (and snapshot, see below).
  • The WAL must not grow without bound; support compaction.

Locking choices

  • Global lock — the baseline above. Simple and obviously correct.
  • Read-write lock — for read-heavy workloads, many gets run together; writes stay exclusive.
  • Sharded / striped locks — partition keys into N shards each with its own lock so writes to different shards run in parallel. This is the simplified ConcurrentHashMap idea.
  • Single writer thread — all writes go through a queue to one writer that owns the WAL; easy to reason about durability, and fsync can be batched.

WAL compaction and recovery

def compact(self):
    with self._lock:
        with open("snapshot.tmp", "w") as f:
            for key, value in self._map.items():
                f.write(encode({"key": key, "value": value}) + "\n")
            f.flush()
            os.fsync(f.fileno())
        os.rename("snapshot.tmp", "snapshot")   # atomic swap
        self._truncate_wal()                     # old log no longer needed

def recover(self):
    self._map = {}
    if snapshot_exists():
        for rec in read_snapshot():
            self._map[rec.key] = rec.value
    for rec in read_wal():                       # replay log after the snapshot
        apply(rec, self._map)

The WAL accumulates dead history (PUT a 1, PUT a 2, PUT a 3...). Compaction writes the live map to a temp snapshot, fsyncs it, atomically renames it into place, then truncates the WAL — all crash-safe. On recovery you load the snapshot first, then replay only the WAL entries written after it.

Think about it

You update the in-memory map first, then crash before the WAL reaches the disk.

On recovery, what does the store now believe — and how is that different from what the caller was already told?

Why does flipping the order to "WAL first, then memory" fix it?

Follow-ups

  • Every write pays its own fsync. How would group commit let several writes share one — and what do callers give up?

  • The WAL grows forever. When should compaction kick in, and how do you keep it crash-safe while writes are still coming?

  • How would a copy-on-write snapshot let readers run without ever blocking a writer?

  • And more...

FANG Crash Course

Fullset Newest FANG Coding/System Design Questions, Optimal Solutions and Explanations.

Step_1 Matched with a FAANG+ Senior Engineer

Step_2 Fullset FANG Coding/System Design Questions

Step_3 Solutions + step by step Explanation

Check it out