← back to projects
~/projects/redis-clone active

Redis Clone (C++)

A from-scratch reimplementation of Redis's core: a RESP protocol parser, an in-memory key-value store with TTL/expiry via lazy deletion, and a socket-based server handling concurrent clients.

C++20CMake

Highlights

  • $ Implemented a RESP (REdis Serialization Protocol) parser to handle the wire format used by real Redis clients.
  • $ Built a key-value datastore wrapping std::unordered_map inside a dedicated Datastore class.
  • $ Added TTL/expiry support using std::optional<std::chrono::steady_clock::time_point>, with lazy deletion on access.
  • $ Structured the project as a proper multi-file C++ build — header/source splits, one-definition rule — managed with CMake.
  • $ Built the networking layer to handle multiple concurrent client connections.

Architecture

Redis Server — single process, multi-threaded
Clients
TCP Connection
src/main.cpp
socket()
bind()
listen(6379)
Accept Loop
accept()
Per-Client Handler Thread
std::thread (detached)
handle_client (src/main.cpp)
recv()
Raw bytes from socket
RESP Parser
src/resp_parser.cpp
Tokenize raw bytes
Parsed Command
["SET","key","value","EX","10"]
Command Dispatcher
src/command_handlers.cpp
Route to command handler
RESP Reply
+OK\r\n, $5\r\nvalue\r\n
send()
Reply back to client
In-Memory Datastore — src/datastore.h
class Datastore
std::unordered_map<string, MapValue>
struct MapValue
std::string value;
optional<chrono::steady_clock::time_point> expiry
Operations
  • $ set(key, value, expiry)
  • $ get(key) -> std::string
  • $ has_key(key) -> bool
  • $ TTL: lazy expiry on access

Trade-offs & decisions

Lazy deletion vs. active expiry sweeping

Expired keys are checked and evicted on access via std::optional<steady_clock::time_point>, rather than run by a background sweep thread. Simpler and avoids extra synchronization, at the cost of expired-but-unaccessed keys sitting in memory a little longer than strictly necessary.

Encapsulated Datastore class vs. raw map exposure

Wrapping unordered_map in a Datastore class keeps TTL logic and future thread-safety changes (locking, sharding) contained to one place, at a small cost of an extra indirection layer for every read/write.

Code excerpt

main.cpp
// Accept loop and Client Handling

// server_fd is established earlier in main.cpp

while(true){
    struct sockaddr_in client_addr;
    int client_addr_len = sizeof(client_addr);
    std::cout << "Waiting for a client to connect...";

    int client_fd = accept(server_fd, (struct sockaddr *) &client_addr, (socklen_t *) &client_addr_len);

    std::thread worker(handle_client, client_fd, std::ref(data));
    worker.detach();

  }

void handle_client(int client_fd, Datastore& data)
{
  char buffer[1024];
  std::cout << "[Thread " << std::this_thread::get_id() <<"] Client Conneted via socket: " << client_fd << std::endl;

  while(true){
    std::memset(buffer, 0, sizeof(buffer));
    int bytes_received = recv(client_fd, buffer, sizeof(buffer), 0);
    if (bytes_received <= 0){
      std::cout << "[Thread " << std::this_thread::get_id() <<"] Client disconnected or error occured";
    }

    std::cout << "Processing message..." << std::endl;
    std::vector<std::string> message {parse_bulk_string(buffer)};
    std::string response {handle_received(message, data)};
    std::cout << response << std::endl;
    send(client_fd, response.c_str(), response.size(), 0);

  }

  close(client_fd);
}

Live demo

live-demo — wasm console

This runs the resp_parser.cpp, command_handlers.cpp, and datastore.h compiled to WebAssembly. The TCP socket layer from main.cpp can't run in a browser, so it's replaced with a direct call straight into the compiled module.

wasm module loading…
redis>
try: