Build a Crash-Consistent Single-Server Key-Value Store
BasetenSoftware Engineer · Technical ScreenLast updated
Design and implement a thread-safe, crash-consistent key-value storage engine that persists byte-string records to an append-only log on disk, recovers from partial writes, and supports tombstones.
Problem Statement
Implement an embedded, log-structured key-value store (LogKV) for arbitrary byte keys and byte values. The store persists data to an append-only log file on a local filesystem and maintains an in-memory index for fast point lookups.
The system must guarantee crash consistency: in the event of an abrupt process crash, power loss, or torn write mid-record, the recovery mechanism must restore the database to the last clean, uncorrupted record state.
Requirements
- Record Framing & Integrity:
- Each record written to the append-only log must contain a fixed-width header followed by the payload.
- Header format (13 bytes total, big-endian):
crc32(4 bytes, unsigned int): Checksum computed overflags + key_len + val_len + key + value.flags(1 byte): Record type (0x01forPUT,0x02forTOMBSTONE/ deletion).key_len(4 bytes, unsigned int): Length of the key in bytes.val_len(4 bytes, unsigned int): Length of the value in bytes (0 for tombstones).
- Payload:
keybytes immediately followed byvaluebytes.
- Operations:
put(key, value): Append aPUTrecord, flush/sync to disk, and update the in-memory index.get(key): Retrieve the latest value from the in-memory index or disk. ReturnNoneif the key does not exist or was deleted.delete(key): Append aTOMBSTONErecord, flush/sync to disk, and remove the key from the in-memory index. ReturnTrueif key existed,Falseotherwise.recover(): Invoked at initialization. Scans the log from offset 0, validates CRC checksums, discards corrupt/partial trailing writes by truncating the file to the last valid boundary, and rebuilds the index.
- Concurrency:
- Thread-safe for concurrent readers and writers.
Class Interface
class LogKV:
def __init__(self, filepath: str):
"""
Initialize the store at the given file path.
Must call recover() if the file already exists.
"""
pass
def put(self, key: bytes, value: bytes) -> None:
"""Persist key-value pair and update index."""
pass
def get(self, key: bytes) -> bytes | None:
"""Return value for key, or None if not found or deleted."""
pass
def delete(self, key: bytes) -> bool:
"""Append tombstone and remove key from index. Return True if existed."""
pass
def recover(self) -> None:
"""Scan log, validate records, truncate corrupted tail, and rebuild index."""
pass
def close(self) -> None:
"""Flush buffers and close underlying file descriptors."""
pass
Example
store = LogKV("data.db")
store.put(b"user:101", b"Alice")
store.put(b"user:102", b"Bob")
assert store.get(b"user:101") == b"Alice"
store.delete(b"user:101")
assert store.get(b"user:101") is None
store.close()
# Simulate crash recovery on reopen
reopened_store = LogKV("data.db")
assert reopened_store.get(b"user:101") is None
assert reopened_store.get(b"user:102") == b"Bob"
Constraints & Edge Cases
1 <= len(key) <= 65,536bytes.0 <= len(value) <= 10,485,760(10 MB) bytes.- Recovery must handle torn writes (e.g., partial header, header present but payload truncated, or CRC mismatch due to corrupted bits). Once an invalid record is encountered, the log must be truncated at that exact record boundary, discarding all subsequent bytes.
- Handle empty log files gracefully.
- Deleting a non-existent key must return
Falsewithout writing an unnecessary tombstone.
Hints
Hint 1
Maintain an append-only log on disk and an in-memory hash map mapping keys to byte offsets and lengths (Bitcask pattern) to avoid holding entire values in memory.
Hint 2
Compute a CRC32 checksum over the record attributes and payload; on recovery, read sequentially and compute CRC on each record to ensure it matches the stored checksum before applying it to the index.
Hint 3
If `read` returns fewer bytes than expected or the CRC check fails during recovery, issue an `os.ftruncate` at the last clean record offset to restore integrity.
Follow-up questions
- How would you implement background log compaction (garbage collection of superseded keys and tombstones) without stalling active reader and writer threads?
- Every synchronous `fsync` per write incurs a high latency penalty. How would you design a group-commit batching mechanism?
- How would you shard the log and lock structures to support high-throughput writes across many concurrent CPU cores?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.