I built a small Redis clone in C: a RESP parser, a command table, an append-only file for persistence. Recently I started building the same thing again in Rust, and rebuilding a project I had already finished has taught me more Rust than any from-scratch tutorial.
The reason is simple. The second time, the design is already solved. I know what the AOF has to guarantee, what the command table dispatches, what the parser must reject. So none of my attention goes to what to build. All of it goes to how Rust wants it built.
That turns the domain into a constant and the language into the only variable. Every difference I hit is pure signal about Rust, not noise about key-value stores.
The first difference shows up before any logic runs. In C, I built the substrate first: my own dynamic strings, my own hashmap, my own linked list. Hundreds of lines before a single command worked. In Rust, Vec, String, and HashMap are just there, so that whole layer disappears and I start at the actual command logic. A standard library quietly decides where your project even begins.
The sharper difference is in dispatch. In C it is a switch with argument counts I check by hand:
if (argc != 3) return err("wrong arg count");
switch (cmd) {
case CMD_SET: return do_set(argv[1], argv[2]);
case CMD_GET: return do_get(argv[1]);
/* forget a case and it is a runtime bug */
}
In Rust the same dispatch is an enum and a match, and the compiler will not build until every case is handled:
match cmd {
Command::Set { key, val } => self.set(key, val),
Command::Get { key } => self.get(key),
}
Same dispatch. One version cannot ship the missing-case bug I actually shipped in C.
If you already know a project cold, rebuild it in the language you are learning. You stop thinking about the problem and start feeling the language.
Top comments (4)
Interesting read! I love the idea of rebuilding a solved design to learn the language.
I'm curious about how you managed the file I/O, though. You mentioned it's an append-only file, but does your implementation include coalescing to handle file growth, or is it a simple continuous append log?
Also, I'd love to hear more about how you plan to use Rust's memory management and concurrency guarantees as you expand this project!😁
Hi, i have in plans working on coalescing and compaction strategies for AOF next.
It's pretty tricky, especially trying to make it atomically without losing any data
The standard library quietly decides where your project even begins â this is the real insight, and it extends past the stdlib.
In C, you build substrate first because there's no other option. Every dynamic string, every hashmap, every linked list is yours to implement and debug before you touch the actual command logic. That's not just overhead â it's a design decision baked into the language. Your project starts at layer zero.
Rust doesn't just give you Vec and HashMap. It gives you ownership rules that prevent certain designs from existing. The match-enum dispatch you show isn't just safer than switch â it makes an entire class of runtime bugs impossible to express. The missing-case bug you shipped in C can't be written in Rust because the compiler rejects it. That's the standard library deciding where your project ends, too.
When you build embedded storage in Rust (we've been doing this for a Rust-native multimodal DB), the ownership model forces you to confront a question C never asks: who owns a record? In C, a pointer is a pointer. In Rust, a reference has a lifetime, and that lifetime is a design contract. You can't skip it. You can't hand-wave it. The compiler treats it as architecture, not implementation detail.
What was the most surprising ownership constraint you hit that C just didn't surface?
This is a great argument for “same domain, new language” learning.
The database clone angle is especially useful because the correctness constraints are already concrete: parser rejection rules, command dispatch, persistence guarantees, replay behavior. When those are fixed, the language differences become impossible to hand-wave.
The enum/match example is the strongest one. It is not just nicer syntax; it changes where a whole class of bugs gets caught. For storage projects, that matters a lot because many mistakes only appear after restart, replay, or corrupted input. A compiler forcing exhaustiveness before those states exist at runtime is a real design advantage.