Skip to main content
In this article, we demonstrate how to build a lightweight key-value store database with persistent storage and a network interface for client-server communication. This server supports three core operations—GET, SET, and DELETE—while processing multiple client requests concurrently and ensuring that data is not lost during server restarts by persisting it to disk.

Functional Requirements

The primary operations of the database include:
  • GET: Retrieve the value associated with a specified key.
  • SET: Store or update a key-value pair.
  • DELETE: Remove a key-value pair.
Data persistence is achieved by writing to disk (using formats like JSON or a custom binary format), ensuring survival through server restarts. The server supports concurrent client connections, robust error handling for invalid commands, malformed requests, and I/O errors, and is designed with extensibility in mind.
The image outlines functional requirements for a system, including core operations, persistence, concurrency, networking, and error handling. Each section provides specific tasks or features related to the system's functionality.

Non-functional Requirements

  • Performance: Handle a moderate number of concurrent client connections.
  • Reliability: Ensure data integrity through robust error handling and periodic snapshots.
  • Simplicity: Utilize a minimal and easy-to-understand design.
  • Security: Validate inputs to safely manage malicious or malformed requests.

High-Level Architecture

The system is organized into several key components:
  1. Networking Layer: Manages incoming TCP connections using an asynchronous runtime (e.g., Tokio).
  2. Command Parser: Transforms raw client input into validated and structured commands.
  3. Core Key-Value Store: Maintains an in-memory data structure with thread-safe access.
  4. Persistence Module: Persists snapshots of the in-memory data to disk and reloads them on startup.
  5. Command Execution Engine: Processes the commands by interfacing with both the key-value store and the persistence module.
The image outlines the high-level architecture of a system, detailing key components such as the Networking Layer, Command Parser, Core Key-Value Store, Persistence Module, and Command Execution Engine, along with their functions.
Error handling spans across network, file, I/O, and command-related errors for graceful operation. The typical data flow in the system is as follows:
  1. A client sends a command (e.g., GET, SET, DELETE) over TCP.
  2. The networking layer reads the incoming request asynchronously.
  3. The command parser converts the raw input into a structured command.
  4. The command execution engine processes the command against the key-value store.
    • For SET and DELETE, changes are applied in-memory and immediately persisted to disk.
  5. A response such as “OK”, “ERROR”, or the retrieved value is sent back to the client.

API Commands

The server supports the following commands:
  • GET key
    Retrieves the value associated with the specified key.
  • SET key value
    Stores the provided key-value pair. The server responds with “OK” upon success.
  • DELETE key
    Removes the key-value pair, returning “OK” on successful deletion.
For unrecognized commands, the server replies with an error message. Example interaction:

Design Considerations

Key design aspects of this project include:
  • Thread Safety: Utilize mutexes to manage concurrent read/write operations securely.
  • Data Durability: Persist updates immediately to disk and perform periodic snapshots.
  • Error Handling: Provide descriptive errors for invalid or malformed inputs.
  • Scalability: Process multiple client requests concurrently using asynchronous programming.
The image outlines four key design considerations: Thread Safety, Data Durability, Error Handling, and Scalability, each with a brief description.

Technology Stack

The image lists a technology stack including "Async Runtime" with "tokio," "TCP Networking" with "tokio::net," and "Serialization" with "serde and serde_json."
For more details on these technologies, refer to Kubernetes Basics.

Setting Up the Project

Begin by creating a new Rust project using Cargo. Update your Cargo.toml file with the necessary dependencies:
The anyhow crate simplifies error handling using trait object–based error types.

Networking Layer Implementation

The networking layer handles incoming TCP connections concurrently. The code below shows the main server logic along with the client-handling function.

Main Function

Handle Client Function

In this implementation, each accepted TCP connection is processed asynchronously by spawning a new task. The server reads incoming data line by line using a buffered reader, parses the commands, executes them against the key-value store, and sends back the response.

Command Parsing

The command parser converts raw string inputs into structured commands for the system to process. Create a file named command.rs with the following content:
This parser splits the input into at most three parts (command, key, and value) and then maps them to the corresponding command variant.

Key-Value Store Implementation

The in-memory key-value store is implemented using Rust’s asynchronous mutex for safe concurrency. Create a file named store.rs with the following code:

Persistence Module

To ensure data durability between server restarts, create a file named persistence.rs. This module extends KeyValueStore with methods to load from and save data to disk using JSON serialization with asynchronous file I/O provided by Tokio.
The load method checks if the target file exists. If it does, the file is read and the JSON content is parsed to populate the in-memory store. In contrast, the save method creates a snapshot of the current state and writes it to disk without holding any locks across asynchronous calls.

Testing the Application

After running the application with:
you can test the server using Telnet:
  1. Connect to the server:
  2. Use these commands to interact with the server:
    • GET x → Expected response: ERROR Key not found
    • SET x 100 → Expected response: OK
    • GET x → Expected response: VALUE 100
    • DELETE x → Expected response: OK
    • For an unknown command like FOO, the server replies with ERROR Unknown Command
Data persistence is verified by checking that a data.json file is created and properly populated. Restarting the server will reload the stored keys automatically. Example content of data.json after some operations:
A sample Telnet session might look like this:
When you terminate the program with Ctrl+C, the server saves the current state to disk, allowing for a seamless restart.
Congratulations! You have successfully implemented a robust key-value store with comprehensive networking, command parsing, thread-safe in-memory storage, and persistence. This project demonstrates key Rust programming principles and asynchronous programming using Tokio.

Watch Video