Skip to content

Overview — Project 2: kvlite

In Project 1 · logwise you built a program that reads data, runs to completion, and exits. kvlite is the opposite shape: a program that holds data, stays running, and is talked to by many clients at once. That single change — long-lived shared state — is what pulls in the next tier of Rust: traits and generics to make the store reusable, Arc/RwLock to share it across threads without a data race, an append-only log so a restart doesn’t lose everything, and a TCP server to put it on the network. You’ll meet each one exactly when the build demands it.

A small key-value store — a mini-Redis. By the end it speaks a plain-text line protocol you can drive with nc:

$ nc 127.0.0.1 6380
SET name ada lovelace
OK
GET name
VALUE ada lovelace
DEL name
DELETED
GET name
NIL

Under that one-line protocol sits a layered design, and each layer is one day’s lesson:

TCP server day 7 std::net, thread-per-connection, the line protocol
protocol day 7 parse a line -> Request -> Response
Db day 6 thread-safe + durable: SharedStore + append-only log
╱ ╲
store.rs log.rs day 4/5: Store trait + MemStore + SharedStore / day 6: the WAL
DayPageYou buildThe Rust it forces
4Traits & genericsa single-threaded store + a REPLtraits, generics, lifetimes-in-context, HashMap
5Concurrencyconcurrent access from many threadsArc, Mutex/RwLock, mpsc, Send/Sync
6Persistence & errorssurvive a restartappend-only log, iterators, thiserror
7TCP servera networked kvlitestd::net, thread-per-connection, the protocol

Each day teaches its concept, then has you build that slice into the companion crate at rust/kvlite/. Every page sets up its files first — as compiling stubs — then fills in the behavior, so the crate is green at the end of every day. Type the code yourself, run it, and break it on purpose — the compiler’s objection is the lesson.

Unlike logwise, which was a single binary, kvlite is a library with a thin binary on top: all the logic lives in the library (src/lib.rs and its modules) so both the tests and the kvlite binary use exactly the same code, and src/main.rs is a small CLI wrapper. The modules mirror the layer diagram above, and each is filled in on the day noted:

rust/kvlite/
├─ Cargo.toml ← one dependency: thiserror (everything else is std)
├─ src/
│ ├─ lib.rs ← module declarations + the names re-exported for users
│ ├─ error.rs ← KvError + the crate-wide `Result<T>` alias (Day 4, grows Day 6)
│ ├─ store.rs ← Store trait · MemStore · SharedStore (Day 4–5)
│ ├─ log.rs ← Command · validate_key · Wal · replay() (Day 6)
│ ├─ db.rs ← Db: SharedStore + Wal, write-ahead durable (Day 6)
│ ├─ protocol.rs ← Request · Response · handle_line (Day 7)
│ ├─ server.rs ← serve / handle_connection (thread-per-conn) (Day 7)
│ └─ main.rs ← the `kvlite` binary: serve & repl subcommands (Day 4, final Day 7)
└─ tests/
├─ concurrency.rs ← many threads hammer one shared store (Day 5)
└─ server.rs ← a real client drives the real socket (Day 7)

The dependencies point one way — serverprotocoldb{store, log}error — so we build bottom-up: the Store trait and error first (Day 4), then concurrency (Day 5), then durability (Day 6), then the network on top (Day 7). Nothing you write ever points at a file that doesn’t exist yet.

You only need a Rust toolchain (rustup gives you cargo). From the crate directory:

Terminal window
cd rust/kvlite
cargo test # every unit + integration test, all std
cargo run -- repl # talk to the store over stdin, no socket
cargo run -- serve # start the TCP server on 127.0.0.1:6380
cargo run -- serve 127.0.0.1:7000 --wal /tmp/my.wal # custom address + log path

With the server running, drive it from another terminal exactly as a client would:

Terminal window
nc 127.0.0.1 6380
SET name ada lovelace
GET name

Why a key-value store is the right teacher

Section titled “Why a key-value store is the right teacher”

A KV store is the smallest program that is simultaneously shared, mutable, long-lived, and durable — the four properties that make real systems hard. Hold all four at once and every big Rust idea has a concrete reason to exist:

  • Shared + mutable → the borrow checker’s “shared XOR mutable” rule (from The Rust Mindset) now has to hold across threads. That’s Arc<RwLock<T>>, and the compiler enforces it with the Send/Sync traits.
  • Reusable → you don’t want to rewrite the engine for every key/value type, so the store is a trait with a generic implementation.
  • Durable → RAM is wiped on exit, so writes go to an append-only log you replay on startup — a job tailor-made for iterators.

Hold the playbook’s recurring question on every page: what does building this force you to understand — and what is Rust’s compiler protecting you from? For kvlite the answer sharpens to its most famous form. The moment two threads touch one HashMap, C would let you race; Rust simply won’t compile the shared-mutable access until you put it behind a lock — fearless concurrency, checked at compile time. By Day 7 you’ll have a networked store where the scariest class of bug in systems programming was made unwritable by the type system.

Begin with Day 4 · Traits & Generics →