Building IronCache: A Redis-like Database in Rust

Muhammad

Muhammad Farrell Hauzan

Building a Redis-like In-Memory Database From Scratch in Rust

I’ve always loved learning new things, and after three years as a software engineer, I was ready to find my next big challenge.

My “Why”: From Professional Coder to Rust Beginner

As a software engineer with three years of experience, I’ve always loved learning new things and pushing my boundaries. I was ready for a new challenge that would force me to think differently about code, and I wanted to dive deep into systems programming to understand how high-performance software is built. That’s when I kept hearing about Rust and its promises of performance, reliability, and memory safety without a garbage collector.
I quickly hit the limits of what tutorials could teach me. To truly learn Rust, I knew I had to move beyond exercises and build something real. To get a structured path forward, I asked Gemini to create a step-by-step learning plan centered around a single, substantial project. My criteria were simple: find a project that would force me to battle-test Rust’s core concepts — like concurrency and memory management — while still being achievable. A project where I could build, refactor, and see a tangible result.
That’s how IronCache was born. It’s my journey into Rust, guided by that plan and documented through the process of building a Redis-like in-memory database from the ground up.

So, What is IronCache?

I named my project IronCache — partly because it’s a persistent key-value store, and partly because “Iron” just felt fitting for a Rust project (if you know, you know 🦀). It’s my take on a concurrent, persistent key-value store that mimics some of Redis’s core functionality, and it was my vehicle for learning several critical systems programming concepts:
Concurrent TCP Server: Handles multiple client connections simultaneously.
Multi-data Type Support: Stores Strings, Lists, and Hashes.
Data Persistence: Automatically saves snapshots and recovers on startup.
Key Expiration: Supports Time-To-Live (TTL) on keys.

My “Aha!” Moments: The Key Concepts That Clicked

Building this project was a series of challenges and breakthroughs. Here are the five key Rust concepts that went from being abstract theory to practical tools in my arsenal.

1. Finally Understanding Ownership in Practice

Rust’s ownership and borrow checker are famous, and for good reason. I truly understood their power when building the server. How could multiple clients, each in their own concurrent task, safely modify the same database? The answer became my most-used pattern: Arc<Mutex<T>>.
// This became the core of my shared state managementuse std::sync::{Arc, Mutex};// The shared database type, safe to pass across threadstype Db = Arc<Mutex<Storage>>;// In my main loop, I could give each new task a reference// to the same database without violating ownership rules.let db_clone = db.clone();tokio::spawn(async move {    // This background task can now safely access the shared state.    handle_client(socket, db_clone).await;});
My Takeaway: Arc<Mutex<T>> isn't just syntax; it's a way of thinking. It forces you to be explicit about shared ownership and controlled mutation, which felt like a superpower once it clicked.

2. The Elegance of Async Programming with Tokio

Building a server that doesn’t freeze while waiting for one client was my goal. Tokio’s async ecosystem made this surprisingly clean.
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> {    let listener = TcpListener::bind("127.0.0.1:6969").await?;    loop {        let (socket, _) = listener.accept().await?;        let db = db.clone();        // Spawn a new, independent task for each connection.        // The main loop immediately goes back to accepting more clients.        tokio::spawn(async move {            handle_client(socket, db).await;        });    }}
My Takeaway: async/await and tokio::spawn are a powerful combination. It allows you to write concurrent code that looks almost sequential, making complex network logic much easier to reason about.

3. Making My Code Smarter with Enums and Pattern Matching

Coming from other languages, I initially thought of enums as just simple lists of variants. Rust showed me they are so much more. Defining my server’s commands with an enum made my code incredibly robust and self-documenting.
// An enum that carries the data for each commandpub enum Command {    Get { key: String },    Set { key: String, value: String, expiry: Option<Duration> },    // ... more commands}// Pattern matching makes handling commands so clean and safe.// The compiler ensures I handle every possible command variant.match command {    Command::Get { key } => handle_get(key, &db).await,    Command::Set { key, value, expiry } => handle_set(key, value, expiry, &db).await,    // ...}
My Takeaway: This was a huge level-up for me. Coming from my background in TypeScript, Go, and Ruby, using these powerful enums with match just felt so much cleaner. It eliminated entire classes of bugs and made my application logic type-safe and easy to follow.

4. Effortless Serialization with Serde

For persistence, I needed to save my in-memory HashMap to a file. I braced myself for a lot of manual serialization code, but serde made it feel like magic.
use serde::{Serialize, Deserialize};#[derive(Serialize, Deserialize, Debug)]pub struct Storage {    data: HashMap<String, StoreValue>,    // I can even tell Serde to ignore runtime-only fields!    #[serde(skip)]    dirty: bool,}
My Takeaway: Wow, I had no idea a library like this existed in Rust. Coming from languages where I’d have to write a ton of boilerplate for this, Serde felt like magic. The ability to just #[derive] serialization is a massive productivity boost.

5. Error Handling That I Couldn’t Ignore

Rust’s Result<T, E> forces you to handle potential failures. Initially, this felt verbose compared to exceptions, but it quickly made my code far more resilient.
// My parser returns a Result, forcing the caller to handle parse errors.pub fn parse(buffer: &[u8]) -> Result<Command, ParseError> {    match parts.as_slice() {        ["SET", key, value] => Ok(Command::Set { /* ... */ }),        _ => Err(ParseError::UnknownCommand),    }}// In my main loop, I can handle it cleanly.let command_result = Command::parse(&buffer);match command_result {    Ok(command) => execute_command(command, &db).await,    Err(e) => format!("ERROR: {:?}\n", e),}
My Takeaway: The ? operator and Result types create a clear "happy path" while making sure you never forget to handle the error cases. It's a fundamental shift that leads to more reliable software.

Features I’m Most Proud Of

Efficient Periodic Persistence: Instead of writing to disk on every command, I implemented a “dirty” flag. A background task wakes up every 10 seconds and only saves the database if data has actually changed, which is much more efficient.
Lazy Expiration for Keys: To avoid scanning the whole database for expired keys, I implemented lazy eviction. A key’s expiry is only checked when a client tries to GET it. If it's expired, it's removed at that moment.
A Modular Architecture: I intentionally refactored the project to separate concerns: - storage.rs: Manages data in memory. - commands.rs: Defines and parses commands. - main.rs: Handles the TCP server and coordinates the other modules.

What’s Next?

This project was a huge learning experience, but what’s next is anyone’s guess! I might continue with IronCache and refactor the networking layer to use the official Redis Serialization Protocol (RESP)… or I might get distracted by another shiny new idea. My project backlog has more forks than a fancy dinner party, so we’ll see which path wins.
If I do stick with it, getting redis-cli to talk to IronCache would be the next big win. Got any other interesting ideas for a new project? Let me know in the comments!

Final Thoughts

Building IronCache taught me more about systems programming than any book could have. It forced me to confront Rust’s most challenging concepts and, in doing so, showed me their immense value.
If you’re on the fence about learning Rust, I can’t recommend the “learn by building” approach enough. Find a project that excites you, start simple, and don’t be afraid of the borrow checker — it’s a tough but fair teacher.
The complete source code is available on my GitHub https://github.com/farrellh1/iron_cache. Feel free to check it out, experiment with it, and start your own learning journey!
Happy coding, and welcome to the Rust community! 🦀
Like this project

Posted Jun 18, 2025

Developed IronCache, a Redis-like database in Rust to learn systems programming.

Likes

0

Views

1

Timeline

Jun 8, 2025 - Jun 14, 2025