Building on FoundationDB: A Developer's Guide
FoundationDB gives you a single, ordered, transactional key/value store and asks you to build everything else on top of it. That freedom is the whole point, and the main source of mistakes. This guide is a practical, opinionated walkthrough of how to use this .NET binding (FoundationDB.Client / SnowBank) well: how to encode keys, how transactions actually behave, and how to build sophisticated, distributed "Layers" without falling into the classic traps.
It is organized in four parts:
- Keys, Values & Layers: how data is encoded, and how to package data access into a reusable Layer. Start here.
- Transactions: the retry loop, idempotency, conflicts, atomic operations, and watches.
- Advanced Layers: how the cluster processes a transaction, how to make layers fast, and the hard distributed-systems patterns (change feeds, leases, retention, fencing).
- Binary Data (Slice & Buffers): the byte-level toolkit beneath everything else,
SliceandSliceReader/SliceWriter, pooled buffers, and the integer encodings. Reach for it when you write custom value codecs.
These guides are the human-facing companion to the agent-oriented skills under
.claude/skills/, and every code example mirrors the compile-checked samples insamples/SkillValidation/.
The mental model in one screen
- The database is one flat, sorted map of bytes to bytes: keys sort by their raw bytes, and that ordering is the only structure you get.
- Tuples are the default key encoding: they turn typed values into bytes whose order matches the logical order of the values.
- A subspace is a key prefix you get by resolving a logical location (usually through the Directory layer). All your keys live inside it.
- A transaction is serializable and ACID, but may need to be retried, and is bounded to 5 seconds and 10 MB of writes.
- A Layer is a small, reusable component that turns the raw key/value API into a meaningful abstraction (a map, an index, a document store, a change feed).
The big lessons (learned the hard way)
These recur throughout the guide; they're worth internalizing up front.
- Never touch raw bytes. Build keys with
subspace.Key(...)and values withFdbValue.*, and hand those objects straight to the transaction. Manual string/byte concatenation breaks ordering and escaping. - Keys are lazy.
subspace.Key("a", 1)is a small struct that remembers its parts; it renders to bytes only when the transaction needs it. Don't eagerly call.ToSlice(). - Your transaction handler runs more than once. It must be a pure function of database state: no external side effects (caches, counters, logging) inside it.
- Use atomic operations for contention. A single hot counter serializes all writers at the resolver;
AtomicAdd64and sharding don't. - There is no global wall clock. You can't compare clocks from different nodes. When you need a shared notion of time or order, use the database's read version (a monotonic clock from the cluster's sequencer), never
DateTime.UtcNowacross nodes. - Latency is round-trips. The client pipelines, so batch independent reads (
GetValuesAsync,Task.WhenAll) and avoid "read, decide, read again" chains. - Trim unbounded logs, and let consumers detect that they fell behind. A change feed isn't done until it has retention and a way to tell a stalled subscriber to resync.
If a piece of code you're writing or reviewing touches keys, transactions, or multi-node coordination, the relevant guide below has the idiomatic pattern, and the reasoning behind it.