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.
What you’re building
Section titled “What you’re building”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 NILUnder 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 WALThe four days
Section titled “The four days”| Day | Page | You build | The Rust it forces |
|---|---|---|---|
| 4 | Traits & generics | a single-threaded store + a REPL | traits, generics, lifetimes-in-context, HashMap |
| 5 | Concurrency | concurrent access from many threads | Arc, Mutex/RwLock, mpsc, Send/Sync |
| 6 | Persistence & errors | survive a restart | append-only log, iterators, thiserror |
| 7 | TCP server | a networked kvlite | std::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.
How the code is laid out
Section titled “How the code is laid out”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 — server → protocol → db → {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.
How to run it
Section titled “How to run it”You only need a Rust toolchain (rustup gives you cargo). From the crate directory:
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 pathWith the server running, drive it from another terminal exactly as a client would:
nc 127.0.0.1 6380 SET name ada lovelace GET nameWhy 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 theSend/Synctraits. - 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.
The thread
Section titled “The thread”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 →