Skip to content

Building a Key-Value Store: Your First Database Component

Today we’re building the foundation of FerrisDB - a simple key-value store! Think of it like a super-fast notebook where you can store and retrieve information using a unique key.

graph LR
    A[Your App] -->|"set('user:123', 'Alice')"| B[Key-Value Store]
    B -->|"get('user:123')"| A
    B -->|"Returns 'Alice'"| A

Every web app needs fast data access. When someone visits your e-commerce site:

  • You need their shopping cart (key: cart:user123)
  • You need their session (key: session:abc123)
  • You need product details (key: product:789)

Redis solves this problem. Today, we’ll build our own version!

🦀 New Rust Concepts

  • Variables: How Rust handles data
  • Mutability: When data can change
  • Structs: Creating custom types
  • HashMap: Rust’s key-value collection
  • Option: Handling missing values

📚 Database Knowledge

  • Key-Value Model: Simplest database design
  • In-Memory Storage: Trading durability for speed
  • Hash Tables: O(1) performance magic

Before You Start

Required:

  • Rust installed (rustup.rs)
  • Basic programming knowledge (any language)
  • A text editor

Time Needed: ~30 minutes

No Prior Rust Knowledge Required! 🎉

Let’s create our workspace:

Terminal window
# Create a new Rust project
cargo new --lib tutorial-01-kv-store
cd tutorial-01-kv-store
# Open in your editor
code . # or your preferred editor

Let’s start with the simplest possible key-value store:

// In src/lib.rs
// A struct is like a class in other languages
pub struct KeyValueStore {
// For now, we'll add fields in the next step
}

Now let’s add actual storage to our struct:

use std::collections::HashMap;
pub struct KeyValueStore {
// HashMap is like Map in JS or dict in Python
data: HashMap<String, String>,
}

Let’s add a way to create new key-value stores:

impl KeyValueStore {
// This is like a constructor
pub fn new() -> Self {
KeyValueStore {
data: HashMap::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_creates_empty_store() {
let store = KeyValueStore::new();
assert!(store.is_empty());
assert_eq!(store.len(), 0);
}
}

Run it:

Terminal window
cargo test new_creates_empty_store

Now for the core functionality - storing key-value pairs:

impl KeyValueStore {
pub fn new() -> Self {
KeyValueStore {
data: HashMap::new(),
}
}
// Add this method
pub fn set(&mut self, key: String, value: String) {
self.data.insert(key, value);
}
}

Let’s add the ability to get values back:

impl KeyValueStore {
// ... new() and set() methods ...
pub fn get(&self, key: &str) -> Option<String> {
self.data.get(key).cloned()
}
}

Let’s add some helpful methods to check the store’s state:

impl KeyValueStore {
// ... previous methods ...
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}

Let’s put it all together with a comprehensive test:

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_stores_value_and_get_retrieves_it() {
let mut store = KeyValueStore::new();
// Test basic set and get
store.set("user:1".to_string(), "Alice".to_string());
assert_eq!(store.get("user:1"), Some("Alice".to_string()));
// Test missing key
assert_eq!(store.get("user:2"), None);
// Test overwrite
store.set("user:1".to_string(), "Alice Smith".to_string());
assert_eq!(store.get("user:1"), Some("Alice Smith".to_string()));
}
}

Run all tests:

Terminal window
cargo test

Let’s see our key-value store in action outside of tests:

src/main.rs
use tutorial_01_kv_store::KeyValueStore;
fn main() {
let mut store = KeyValueStore::new();
// Store some user data
store.set("user:1".to_string(), "Alice".to_string());
store.set("user:2".to_string(), "Bob".to_string());
// Retrieve and display
println!("Looking up users...");
match store.get("user:1") {
Some(name) => println!("User 1: {}", name),
None => println!("User 1 not found"),
}
// Try a missing key
match store.get("user:999") {
Some(name) => println!("User 999: {}", name),
None => println!("User 999 not found"),
}
}

Run it:

Terminal window
cargo run

You should see:

Looking up users...
User 1: Alice
User 999 not found

Rust has a helpful tool called clippy that suggests improvements. Let’s follow one of its recommendations:

use std::collections::HashMap;
#[derive(Default)] // Add this line
pub struct KeyValueStore {
data: HashMap<String, String>,
}

This automatically implements the Default trait, which provides a standard way to create an empty store.

use std::collections::HashMap;
#[derive(Default)]
pub struct KeyValueStore {
data: HashMap<String, String>,
}
impl KeyValueStore {
pub fn new() -> Self {
KeyValueStore {
data: HashMap::new(),
}
}
pub fn set(&mut self, key: String, value: String) {
self.data.insert(key, value);
}
pub fn get(&self, key: &str) -> Option<String> {
self.data.get(key).cloned()
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}

You’ve successfully built your first database component in Rust!

  • ✅ A working key-value store (like a mini Redis)
  • ✅ Set and get operations
  • ✅ Proper handling of missing keys
  • ✅ Utility methods for counting and checking emptiness
  • ✅ Your first Rust struct and methods
  • 🦀 Structs: Creating custom types
  • 🦀 Mutability: Understanding &mut self vs &self
  • 🦀 HashMap: Using Rust’s built-in collections
  • 🦀 Option<T>: Safe handling of nullable values
  • 🦀 Methods: Adding behavior with impl blocks
  • 📚 Key-Value Model: The simplest database abstraction
  • 📚 Hash Tables: O(1) performance for lookups
  • 📚 In-Memory Storage: Speed vs durability trade-off

You Found Our Secret! 🤫

Tutorial 2 is still in stealth mode. We’re adding the final touches! Drop us a star if you want us to hurry up! ⭐

Practice Challenges

  1. Add a delete() method 2. Add a len() method to count entries 3. Make keys case-insensitive
Terminal window
cargo new --lib tutorial-01-kv-store
cargo test
cargo test test_specific_name
// Creating a struct
pub struct Name { field: Type }
// Adding methods
impl Name {
pub fn method(&self) { } // read-only
pub fn method(&mut self) { } // read-write
}
// Using Option
match result {
Some(value) => // use value,
None => // handle missing,
}

Great job! You’ve mastered the basics of Rust and built your first database component! 🚀