Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Option A — LevelDB API on top of FoundationDB

Pattern: “API layer above LevelDB” — we keep the familiar LevelDB surface (Put/Get/Delete/Batch/Iterator/Snapshot) but the bytes never touch LevelDB. They live in FoundationDB instead.

Why this is interesting

LevelDB is a local embedded KV. FoundationDB is a distributed transactional KV. Both speak (key, value) pairs over ordered byte keys, so the API shapes are nearly identical — but the guarantees underneath are wildly different:

ConceptLevelDBThis layer (over FDB)
PutAppend to MemTable + WAL on local diskSet inside tr.Transact{...}
GetMemTable → immutable tables → SSTstr.Get (consistent across the cluster)
BatchSingle WAL recordOne FDB transaction (cross-key atomic)
RangeLevelDB SST merging iteratortr.GetRange over a Subspace
SnapshotPins on-disk sequence numberSetReadVersion(capturedVersion) (MVCC)

The mapping is almost mechanical, and that’s the point: it makes FDB’s primitives concrete.

Files

layer/
  encoding.go   Subspace: ns + 0x00 + userKey → fdb.Key, plus range helpers
  db.go         Open / Close / Get / Put / Delete
  batch.go      Batch + DB.Write (atomic multi-op)
  iterator.go   Forward+backward cursor over GetRange results
  snapshot.go   MVCC snapshot via captured read version
demo/main.go    End-to-end: CRUD → batch → range → snapshot vs. live reads

Key-space layout

<namespace> 0x00 <userKey>  -> <value>

Subspace (see layer/encoding.go) keeps multiple logical databases isolated inside one FDB cluster. The 0x00 separator + the fact that the user-key portion is appended as opaque bytes gives us the same lexicographic ordering LevelDB users expect.

Mapping each LevelDB op to an FDB call

  • Put(k,v)db.Transact(func(tr){ tr.Set(ns.Pack(k), v) }). Transact handles automatic retry on conflict.
  • Get(k)db.ReadTransact(func(rt){ rt.Get(ns.Pack(k)).Get() }). An empty FDB result (nil) becomes our ErrNotFound.
  • Delete(k)tr.Clear(ns.Pack(k)) (no-op if absent).
  • Batch.Write → a single Transact containing many Set/Clear ops. Either all of them land, or none do.
  • Iterator → materialized list from tr.GetRange(subspace.Range()). Real production code would stream via RangeIterator, but materializing keeps the iterator usable outside a live transaction (the LevelDB shape).
  • Snapshot → capture the read version with tr.GetReadVersion(), then on subsequent reads call tr.SetReadVersion(captured) so FDB serves the data as it was at that point in time.

Running

  1. Start the shared FDB cluster (from the repo root):
    docker compose up -d
    ./scripts/bootstrap-fdb.sh
    
  2. Install the FDB client library on the host (required by the Go bindings, which are CGO-linked to libfdb_c):
    brew install foundationdb        # macOS
    
  3. Run the demo:
    cd option-a-leveldb
    go run ./demo -cluster ../fdb.cluster
    

Expected output (abridged):

Get apple -> red
batch applied (cherry+date inserted, banana deleted)
range scan [a, z):
  apple = red
  cherry = red
  date = brown
snapshot read version = 1234567
live    Get apple   -> green
live    Get cherry  -> layer: not found
snap    Get apple   -> red   (was 'red' at snapshot)
snap    Get cherry  -> red   (err=<nil>)

That last block is the punchline: the snapshot sees the world before our post-snapshot writes, because FDB just gives us older versions on demand.

What this layer intentionally omits

  • Compaction / SST files — there are none. FDB handles storage internally.
  • Bloom filters — not needed; FDB indexes by key range natively.
  • Write batches with sequence numbers — FDB’s transaction commit version plays that role automatically (tr.GetCommittedVersion() after commit).

See ../option-b-leveldb for the inverse experiment: keeping real LevelDB code paths (memtable + SSTs) but storing the SST bytes in FDB.