KVS: Inside the Architecture of a Go Key-Value Store
Note that this post describes v1.0.0 as of 2026-03-18.
pkg/rbt,pkg/lsm, and others have since been removed, and the library path and server layout have changed too, so don’t read what follows as how to use the current version. The new server layout is covered in the RESP2 and Lua post, and durability and clustering in the append log and Raft post.
I released KVS v1.0.0. KVS is a simple in-memory key-value store written in Go that you can import as a Go module or run as a separate server. In this post I’ll briefly go over the structure of v1.0.0 and look more closely at its Red-Black Tree and LSM Tree implementations.
There are already great key-value stores like Redis, LevelDB, and BoltDB, so why build one myself? Simple: learning and experimenting. I wanted to implement the data structures and design decisions I’d only seen in books and docs, and experience the trade-offs firsthand. So it’s written entirely in Go with no external C dependencies, and it supports both library and server use.
v1.0.0 includes the following.
| Feature | Description |
|---|---|
kvs.Store | Base store built on a synchronized map |
pkg/rbt | Red-Black Tree implementation |
pkg/lsm | In-memory LSM Tree implementation |
| CLI | Cobra/Viper based command-line interface |
| Server | HTTP and gRPC servers |
| Distribution | Static documentation site, Homebrew tap |
Overall Structure
Package layout
kvs/
├── kvs.go # Base Store
├── pkg/
│ ├── rbt/ # Red-Black Tree
│ ├── lsm/ # LSM Tree
│ ├── bitset/ # Bitset utility
│ └── cuckoofilter/ # Cuckoo filter
├── api/kvsv1/ # gRPC Protocol Buffers definitions
├── cmd/kvs/ # CLI entry point
└── internal/server/ # HTTP/gRPC servers
Using it as a module
To use it as a library inside a Go program, create a store with kvs.NewStore(). Inside it’s a single map[string]interface{} guarded by a sync.RWMutex.
| |
pkg/rbt and pkg/lsm are standalone packages not wired into Store. In other words, neither the server nor Store uses the trees internally; you pull them in directly where you need them. Roughly:
kvs.Store(map) : Average O(1) lookup. Plain lookups where order doesn’t matter.pkg/rbt: Guaranteed O(log n). When key order matters.pkg/lsm: For experimenting with collecting writes in a memtable and flushing them as sorted segments.
Red-Black Tree
A Red-Black Tree is a binary search tree that colors each node red or black and stays balanced by keeping these rules.
- The root is black.
- Both children of a red node are black.
- Every path from a node down to NIL has the same number of black nodes.
These rules keep the height of the tree at O(log n).
Structure
| |
Instead of fixing the key type, it takes a compare function. The common ones, CompareString, CompareInt, and CompareFloat64, are already in cmp.go.
| |
Insertion
Insertion finds the spot like an ordinary binary search tree and attaches a red node, and if a rule is broken, insertFix restores it with rotations and recoloring.
| |
insertFix loops while the parent is red and handles the three textbook cases. (The case where the parent is a right child is the same code with left and right swapped, so it’s omitted.)
| |
A rotation swaps a parent and child while keeping the in-order traversal order.
Y rotateRight(Y) X
/ \ ───────────────▶ / \
X C A Y
/ \ ◀─────────────── / \
A B rotateLeft(X) B C
Removal is still O(n)
Embarrassingly, Remove isn’t implemented the proper way yet. It collects every entry except the one being removed and rebuilds the tree from scratch, so it takes O(n).
| |
Red-Black Tree deletion has far more cases than insertion, so I built something that’s definitely correct first and plan to replace it once there are enough tests.
| Operation | Time complexity |
|---|---|
| Put | O(log n) |
| Get | O(log n) |
| Remove | O(n) (rebuild) |
| Clear | O(1) |
LSM Tree
An LSM (Log-Structured Merge) Tree collects writes in memory (the memtable) first, writes them out as sorted files once they reach a certain size, and periodically merges the accumulated files (compaction). LevelDB, RocksDB, Cassandra, and others use this approach.
KVS’s pkg/lsm imitates all of this in memory. Instead of writing to disk it flushes to sorted slices (segments), and there’s no compaction yet.
Structure
| |
lsm.New() creates a tree with the default of 4, and lsm.NewWithMemtableLimit(n) with whatever threshold you want. The default is tiny on purpose, so flushes happen often in tests.
Writes and flushes
Writes always go to the memtable only. When the memtable reaches the threshold, its entries are sorted by key into a new segment that’s placed at the front of the segment list.
| |
Reads
Reads check the memtable first, and if the key isn’t there, binary search the segments from newest to oldest. Because the newest segment is checked first, you get the last value written even if the same key is in several segments.
| |
Deletes and tombstones
Existing segments are never modified, so a delete writes a new entry with deleted: true (a tombstone) to the memtable instead of removing the value. A lookup that hits the tombstone first treats the key as missing.
| |
| Operation | Time complexity |
|---|---|
| Put | Average O(1) (O(m log m) when a flush happens) |
| Get | O(k log s) (k = number of segments, s = segment size) |
| Delete | Get + Put |
With no compaction, segments keep growing, and overwritten values and tombstones stay around. The more you write, the slower reads get and the more memory it uses, so it isn’t ready to be a real store yet. Compaction is the next thing to try.
CLI and Servers
CLI
The CLI is built with Cobra and Viper. --config points to a config file Viper can read (YAML, JSON, TOML, etc.).
| |
kvs serve starts the HTTP and gRPC servers together, on :3456 and :3457 by default. The servers use the kvs.Store shown above.
HTTP
| Method | Path | Description |
|---|---|---|
| GET | /healthz | Health check |
| GET | /v1/keys/{key} | Get a value |
| PUT | /v1/keys/{key} | Store a value ({"value": "..."}) |
| DELETE | /v1/keys/{key} | Delete a key |
| |
The PUT body must be JSON of the form {"value": "..."}, and unknown fields get a 400.
gRPC
The Protocol Buffers definitions are in api/kvsv1/kvs.proto.
| |
| |
Installation
| |
Wrapping Up
You could say v1.0.0 is a version where I implemented a Red-Black Tree, an LSM Tree, a CLI, and servers once each on top of a small key-value store. Writing it up, I see plenty of things I’m not happy with. Next I plan to work on:
- Improving Red-Black Tree removal to O(log n)
- Implementing LSM Tree compaction
- Disk persistence
- Clustering
See the KVS GitHub repository and the documentation site for details.