Build a Concurrent TCP Key-Value Server

Build a multi-client TCP key-value server from raw POSIX sockets in C++20.

Introduction

30 Second Summary

Every time you open an app and your profile loads instantly, there is likely a key-value cache sitting between you and the database. These in-memory stores are the backbone of fast web services.

In this project, you will build a concurrent TCP key-value server from scratch using raw POSIX sockets and modern C++20. You'll handle multiple client connections simultaneously, parse a custom text protocol, and protect shared data with reader-writer locks.

What You'll Build

A production-style in-memory cache server that accepts concurrent TCP connections and responds to SET, GET, DEL, EXISTS, and KEYS commands via a custom wire protocol.

By the end of this project, you'll have:

  • A CMake-based C++20 project with modular file structure and zero external dependencies.
  • A thread pool dispatching client connections to worker threads for concurrent handling.
  • A thread-safe key-value store using std::shared_mutex for high-performance reader-writer concurrency.
  • Secret Mission: Add TTL expiration so keys auto-expire after a configurable number of seconds.

Are there any prerequisites?

You should be comfortable with C++20 features like smart pointers, lambdas, move semantics, and structured bindings. A basic understanding of what TCP sockets are (in any language) is helpful.

You also need Xcode Command Line Tools installed on your Mac. This gives you Apple Clang, the C++ compiler used throughout this project.

Before We Start

Before writing any server code, you need a working C++20 toolchain and a clean project structure.

In this step, you'll verify that your compiler and build system are ready, then create the project skeleton that will hold your server code.

In this step, get ready to:
  • Verify your C++20 compiler is available.
  • Confirm CMake is installed (or install it).
  • Create the project directory structure.
Verify your C++20 compiler

Your Mac already has Apple Clang installed through Xcode Command Line Tools. This is the compiler that will turn your C++20 code into an executable binary.

  • Open your terminal by pressing Cmd+Space (macOS) and typing Terminal, then press Enter.
  • Confirm Apple Clang is available by running this command:
clang++ --version

You should see output that includes a version number and a target triple (something like arm64-apple-darwin).

Don't see a version number?

If you see command not found, your Xcode Command Line Tools may not be installed. Run xcode-select --install and follow the on-screen prompts to install them.

Help me install Xcode Command Line Tools

Verify or install CMake

CMake is the build system that will compile all your source files into a single binary. It reads a configuration file and generates the right compiler commands for your platform.

  • Check if CMake is already installed by running:
cmake --version

✔️ I see a version number

CMake is ready to go. Move on to the next substep.

ⓧ Command not found

CMake is not installed yet. Install it through Homebrew by running:

brew install cmake

This will download and install CMake. Once it finishes, verify the install by running:

cmake --version

You should now see a version number in the output.

brew command not found?

If brew is not recognized, you need to install Homebrew first. Visit brew.sh and follow the installation instructions, then run brew install cmake again.

Help me install Homebrew on my Mac

Create the project directory

Every C++ project needs a clear folder structure. You'll create a top-level project folder and a src/ subdirectory where all your source files will live.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir kvserver
cd kvserver
  • Create the source directory by running:
mkdir src
  • Confirm the directory structure exists by running:
ls

You should see src listed in the output.

Don't see the src folder?

Make sure you ran mkdir src from inside the kvserver directory. Run pwd to check your current location. It should end with kvserver.

Help me find my project directory

Your toolchain is verified and your project skeleton is in place. Next up, you'll configure CMake and start writing the server code.

Set Up the Project and CMake

With your toolchain verified and the project directory in place, it is time to configure the build system. CMake reads a configuration file and generates the platform-specific instructions that compile and link your source code into an executable.

Your server has four distinct modules: a key-value store, a thread pool, a protocol parser, and the server itself. You will create skeleton files for each module now and fill them in over the next steps.

In this step, get ready to:
  • Configure CMake for C++20 with strict compiler warnings.
  • Create skeleton source files for every module.
  • Build and run the project to confirm the toolchain works end-to-end.
Create the CMakeLists.txt build file

CMake needs a file called CMakeLists.txt at the root of your project. This file declares which C++ standard to use, which source files to compile, and which warnings to enable.

  • Create a new file called CMakeLists.txt in the root of your kvserver folder and paste the following:
cmake_minimum_required(VERSION 3.20)
project(kvserver LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

add_executable(kvserver
    src/main.cpp
    src/server.cpp
    src/kvstore.cpp
    src/thread_pool.cpp
    src/protocol.cpp
)

target_include_directories(kvserver PRIVATE src)

target_compile_options(kvserver PRIVATE -Wall -Wextra -Wpedantic)

What does this configuration do?

  • cmake_minimum_required(VERSION 3.20) ensures anyone building this project has a CMake version that fully supports C++20.
  • CMAKE_CXX_STANDARD 20 and CMAKE_CXX_STANDARD_REQUIRED ON tell the compiler to use C++20 and fail if it cannot.
  • add_executable lists every .cpp file the compiler needs to produce the kvserver binary.
  • target_compile_options adds -Wall -Wextra -Wpedantic so the compiler catches potential bugs early.
  • Save CMakeLists.txt.

CMake file not recognized later?

The filename is case-sensitive. It must be exactly CMakeLists.txt with that capitalization pattern.

Make sure the file sits in the kvserver/ root, not inside src/.

Help me troubleshoot my CMakeLists.txt

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file before moving on.

ⓧ I'd like to double check the full code

cmake_minimum_required(VERSION 3.20)
project(kvserver LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

add_executable(kvserver
    src/main.cpp
    src/server.cpp
    src/kvstore.cpp
    src/thread_pool.cpp
    src/protocol.cpp
)

target_include_directories(kvserver PRIVATE src)

target_compile_options(kvserver PRIVATE -Wall -Wextra -Wpedantic)
Create the skeleton source files

Each module gets its own header (.hpp) and implementation (.cpp) file. For now, most of these are empty shells that satisfy CMake's file requirements.

  • Create the empty skeleton files by running this command in your terminal from the kvserver directory:
touch src/server.hpp src/server.cpp src/kvstore.hpp src/kvstore.cpp src/thread_pool.hpp src/thread_pool.cpp src/protocol.hpp src/protocol.cpp
  • Confirm the files exist by running:
ls src/

You should see kvstore.cpp, kvstore.hpp, main.cpp, protocol.cpp, protocol.hpp, server.cpp, server.hpp, thread_pool.cpp, and thread_pool.hpp listed.

Missing files in the listing?

Double-check you ran the touch command from the kvserver/ root directory, not from inside src/.

If main.cpp is missing, it should already exist from the previous step. Run touch src/main.cpp to create it.

Help me figure out why files are missing

The linker needs a main function to produce an executable. The other files can stay empty for now, but main.cpp needs minimal content.

  • Open src/main.cpp and paste the following:
// Minimal skeleton - prints hello and exits
#include <iostream>

int main() {
    std::cout << "kvserver skeleton\n";
    return 0;
}

What does this code do?

This is the simplest valid C++ program. It prints a confirmation message and exits cleanly. Once the full server is built, this file will instantiate the KVStore, ThreadPool, and Server objects instead.

  • Save src/main.cpp.

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file before moving on.

ⓧ I'd like to double check the full code

// Minimal skeleton - prints hello and exits
#include <iostream>

int main() {
    std::cout << "kvserver skeleton\n";
    return 0;
}
Build and run the project

CMake uses a two-step process. First it generates build files from your CMakeLists.txt, then it compiles and links the source code.

  • Generate the build system by running this command:
cmake -B build

You should see output ending with a line like -- Build files have been written to: followed by your build directory path.

  • Compile the project by running:
cmake --build build

You should see compilation output with no errors. Warnings about unused parameters in empty files are fine.

  • Run the binary to confirm it works:
./build/kvserver

You should see kvserver skeleton printed to the terminal.

Build failing?

If CMake reports it cannot find a compiler, verify that Xcode Command Line Tools are installed by running xcode-select --install.

If you see errors about missing source files, make sure every file listed in add_executable exists inside src/. Run ls src/ to double-check.

If you see cmake: command not found, reinstall CMake with brew install cmake.

Help me debug my CMake build

Your project skeleton is locked in and the build pipeline works end-to-end. Next up, you will fill in the KVStore class with a thread-safe in-memory data structure.

Build the Thread-Safe Key-Value Store

Your project compiles cleanly from a minimal skeleton. Now it's time to build the core data layer that every other component will depend on.

A key-value store is the heart of systems like Redis and Memcached. Your store needs to handle concurrent access from multiple threads without data corruption. You'll achieve this with a reader-writer lock that allows many simultaneous reads while ensuring writes are exclusive.

In this step, get ready to:
  • Implement a thread-safe KVStore class with reader-writer locking.
  • Build a protocol parser that handles SET, GET, DEL, EXISTS, and KEYS commands.
  • Verify the store works by running a manual test from main.
Implement the KVStore class header

The KVStore class wraps a std::unordered_map behind a std::shared_mutex. This gives you the reader-writer pattern: multiple threads can read simultaneously with a shared lock, but writes take an exclusive lock that blocks everyone else.

  • Open src/kvstore.hpp in VS Code and replace its contents with the following class declaration:
#pragma once

#include <string>
#include <unordered_map>
#include <shared_mutex>
#include <optional>
#include <vector>

class KVStore {
public:
    void set(const std::string& key, const std::string& value);
    std::optional<std::string> get(const std::string& key) const;
    bool del(const std::string& key);
    bool exists(const std::string& key) const;
    std::vector<std::string> keys() const;

private:
    // Reader-writer mutex: shared_lock for reads, unique_lock for writes
    mutable std::shared_mutex mutex_;
    // The actual key-value storage
    std::unordered_map<std::string, std::string> store_;
};

What does this code do?

This header declares the public interface for your store. Five methods cover the operations a key-value store needs: set a value, get it back, delete it, check if it exists, and list all keys.

The mutable keyword on the mutex lets const methods (like get()) still acquire a lock. Without it, the compiler would refuse to let a const function modify the mutex.

Why std::shared_mutex instead of a regular mutex?

A regular std::mutex blocks all threads. Only one thread can hold it at a time, even for reads. In a cache server, reads vastly outnumber writes.

A std::shared_mutex solves this by allowing many threads to hold a shared_lock simultaneously (for reads). When a write needs to happen, it takes a unique_lock that blocks everyone else until it finishes. This is the exact concurrency pattern that interviewers ask about.

  • Save src/kvstore.hpp.
Implement the KVStore methods

Each method acquires the appropriate lock type before touching the map. Read-only methods use std::shared_lock and mutating methods use std::unique_lock.

  • Open src/kvstore.cpp and replace its contents with the full implementation:
#include "kvstore.hpp"

void KVStore::set(const std::string& key, const std::string& value) {
    // Exclusive lock - blocks all other readers and writers
    std::unique_lock lock(mutex_);
    store_[key] = value;
}

std::optional<std::string> KVStore::get(const std::string& key) const {
    // Shared lock - multiple threads can read simultaneously
    std::shared_lock lock(mutex_);
    auto it = store_.find(key);
    if (it == store_.end()) {
        return std::nullopt;
    }
    return it->second;
}

bool KVStore::del(const std::string& key) {
    // Exclusive lock - erase modifies the map
    std::unique_lock lock(mutex_);
    return store_.erase(key) > 0;
}

bool KVStore::exists(const std::string& key) const {
    // Shared lock - read-only check
    std::shared_lock lock(mutex_);
    return store_.contains(key);
}

std::vector<std::string> KVStore::keys() const {
    // Shared lock - iterating without modifying
    std::shared_lock lock(mutex_);
    std::vector<std::string> result;
    result.reserve(store_.size());
    for (const auto& [key, value] : store_) {
        result.push_back(key);
    }
    return result;
}

What does this code do?

  • set() takes a unique lock and inserts or overwrites the key.
  • get() takes a shared lock and returns std::nullopt if the key doesn't exist. This lets the caller distinguish "empty value" from "missing key".
  • del() returns a boolean so the protocol layer can report whether anything was actually removed.
  • keys() uses structured bindings (const auto& [key, value]) to iterate the map and collect just the key names.
  • Save src/kvstore.cpp.

Seeing red squiggles in your editor?

If VS Code shows errors on shared_mutex or optional, your editor's IntelliSense may not be using C++20. Make sure CMAKE_EXPORT_COMPILE_COMMANDS is set to ON in your CMakeLists.txt (it already is). Then re-run cmake -B build so it regenerates the compile commands database.

help me fix C++20 IntelliSense errors in VS Code

✔️ Awesome, I've got everything!

Great. Make sure both src/kvstore.hpp and src/kvstore.cpp are saved.

ⓧ I'd like to double check the full code

#pragma once

#include <string>
#include <unordered_map>
#include <shared_mutex>
#include <optional>
#include <vector>

class KVStore {
public:
    void set(const std::string& key, const std::string& value);
    std::optional<std::string> get(const std::string& key) const;
    bool del(const std::string& key);
    bool exists(const std::string& key) const;
    std::vector<std::string> keys() const;

private:
    mutable std::shared_mutex mutex_;
    std::unordered_map<std::string, std::string> store_;
};
#include "kvstore.hpp"

void KVStore::set(const std::string& key, const std::string& value) {
    std::unique_lock lock(mutex_);
    store_[key] = value;
}

std::optional<std::string> KVStore::get(const std::string& key) const {
    std::shared_lock lock(mutex_);
    auto it = store_.find(key);
    if (it == store_.end()) {
        return std::nullopt;
    }
    return it->second;
}

bool KVStore::del(const std::string& key) {
    std::unique_lock lock(mutex_);
    return store_.erase(key) > 0;
}

bool KVStore::exists(const std::string& key) const {
    std::shared_lock lock(mutex_);
    return store_.contains(key);
}

std::vector<std::string> KVStore::keys() const {
    std::shared_lock lock(mutex_);
    std::vector<std::string> result;
    result.reserve(store_.size());
    for (const auto& [key, value] : store_) {
        result.push_back(key);
    }
    return result;
}
Implement the protocol parser

The protocol parser is the translation layer between raw text commands and your KVStore methods. Clients will send human-readable strings like SET name Alice and the parser converts those into method calls on the store.

  • Open src/protocol.hpp and replace its contents with this class declaration:
#pragma once

#include <string>
#include "kvstore.hpp"

class Protocol {
public:
    // Takes a reference to the shared KVStore
    explicit Protocol(KVStore& store);
    // Parses a command string and returns the response
    std::string execute(const std::string& input);

private:
    KVStore& store_;
};

What does this code do?

The Protocol class holds a reference to a KVStore (not a copy). Every client connection creates its own Protocol instance pointing at the same shared store. The execute() method takes a raw input line, parses it, calls the right store method, and returns a formatted response string.

  • Save src/protocol.hpp.
  • Open src/protocol.cpp and replace its contents with the full parser implementation:
#include "protocol.hpp"
#include <sstream>

Protocol::Protocol(KVStore& store) : store_(store) {}

std::string Protocol::execute(const std::string& input) {
    std::istringstream stream(input);
    std::string command;
    stream >> command;

    if (command.empty()) {
        return "ERROR unknown command\n";
    }

    // Convert command to uppercase for case-insensitive matching
    for (auto& c : command) {
        c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
    }

    if (command == "SET") {
        std::string key, value;
        stream >> key;
        std::getline(stream, value);
        // Trim leading whitespace from value
        if (!value.empty() && value[0] == ' ') {
            value = value.substr(1);
        }
        if (key.empty() || value.empty()) {
            return "ERROR usage: SET key value\n";
        }
        store_.set(key, value);
        return "OK\n";
    }

    if (command == "GET") {
        std::string key;
        stream >> key;
        if (key.empty()) {
            return "ERROR usage: GET key\n";
        }
        auto result = store_.get(key);
        if (result.has_value()) {
            return result.value() + "\n";
        }
        return "NOT_FOUND\n";
    }

    if (command == "DEL") {
        std::string key;
        stream >> key;
        if (key.empty()) {
            return "ERROR usage: DEL key\n";
        }
        bool deleted = store_.del(key);
        return deleted ? "1\n" : "0\n";
    }

    if (command == "EXISTS") {
        std::string key;
        stream >> key;
        if (key.empty()) {
            return "ERROR usage: EXISTS key\n";
        }
        bool found = store_.exists(key);
        return found ? "1\n" : "0\n";
    }

    if (command == "KEYS") {
        auto all_keys = store_.keys();
        if (all_keys.empty()) {
            return "(empty)\n";
        }
        std::string result;
        for (std::size_t i = 0; i < all_keys.size(); ++i) {
            result += std::to_string(i + 1) + ") " + all_keys[i] + "\n";
        }
        return result;
    }

    return "ERROR unknown command: " + command + "\n";
}

What does this code do?

  • The parser reads the first word from the input as the command, then converts it to uppercase so commands are case-insensitive.
  • SET uses std::getline for the value (after extracting the key) so values can contain spaces.
  • Each command checks for missing arguments and returns a usage error if the input is malformed.
  • KEYS returns a numbered list (like Redis) or (empty) when the store has no entries.
  • Save src/protocol.cpp.

Getting a compile error about missing headers?

Make sure src/protocol.hpp includes "kvstore.hpp" (with quotes, not angle brackets). Quotes tell the compiler to search relative to the current file first, which matches the target_include_directories(kvserver PRIVATE src) in your CMakeLists.

help me debug include errors in my C++ project

✔️ Awesome, I've got everything!

Great. Make sure both src/protocol.hpp and src/protocol.cpp are saved.

ⓧ I'd like to double check the full code

#pragma once

#include <string>
#include "kvstore.hpp"

class Protocol {
public:
    explicit Protocol(KVStore& store);
    std::string execute(const std::string& input);

private:
    KVStore& store_;
};
#include "protocol.hpp"
#include <sstream>

Protocol::Protocol(KVStore& store) : store_(store) {}

std::string Protocol::execute(const std::string& input) {
    std::istringstream stream(input);
    std::string command;
    stream >> command;

    if (command.empty()) {
        return "ERROR unknown command\n";
    }

    // Convert command to uppercase for case-insensitive matching
    for (auto& c : command) {
        c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
    }

    if (command == "SET") {
        std::string key, value;
        stream >> key;
        std::getline(stream, value);
        // Trim leading whitespace from value
        if (!value.empty() && value[0] == ' ') {
            value = value.substr(1);
        }
        if (key.empty() || value.empty()) {
            return "ERROR usage: SET key value\n";
        }
        store_.set(key, value);
        return "OK\n";
    }

    if (command == "GET") {
        std::string key;
        stream >> key;
        if (key.empty()) {
            return "ERROR usage: GET key\n";
        }
        auto result = store_.get(key);
        if (result.has_value()) {
            return result.value() + "\n";
        }
        return "NOT_FOUND\n";
    }

    if (command == "DEL") {
        std::string key;
        stream >> key;
        if (key.empty()) {
            return "ERROR usage: DEL key\n";
        }
        bool deleted = store_.del(key);
        return deleted ? "1\n" : "0\n";
    }

    if (command == "EXISTS") {
        std::string key;
        stream >> key;
        if (key.empty()) {
            return "ERROR usage: EXISTS key\n";
        }
        bool found = store_.exists(key);
        return found ? "1\n" : "0\n";
    }

    if (command == "KEYS") {
        auto all_keys = store_.keys();
        if (all_keys.empty()) {
            return "(empty)\n";
        }
        std::string result;
        for (std::size_t i = 0; i < all_keys.size(); ++i) {
            result += std::to_string(i + 1) + ") " + all_keys[i] + "\n";
        }
        return result;
    }

    return "ERROR unknown command: " + command + "\n";
}
Unit test the store manually from main

Before wiring up networking, you want proof that the store actually works. A quick manual test from main.cpp confirms the build compiles and the store returns correct values.

  • Open src/main.cpp and replace its contents with this manual test:
// Manual KVStore test
#include "kvstore.hpp"
#include <iostream>

int main() {
    KVStore store;
    // Insert a key-value pair and retrieve it
    store.set("hello", "world");
    auto v = store.get("hello");
    std::cout << (v.has_value() ? v.value() : "NOT_FOUND") << "\n";
    return 0;
}

What does this code do?

This is a minimal smoke test. It creates a store, sets the key hello to world, retrieves it, and prints the result. If you see world in the terminal, the store and its locking logic are working correctly.

  • Save src/main.cpp.
  • Rebuild the project by running:
cmake --build build
  • Run the binary to test your store:
./build/kvserver
  • You should see world printed to the terminal.

Build failing with undefined reference errors?

Check that your CMakeLists.txt still lists src/kvstore.cpp and src/protocol.cpp in the add_executable list. If the linker can't find the symbols, it means the implementation files aren't being compiled into the binary.

Also verify that src/server.cpp and src/thread_pool.cpp still exist as empty skeleton files. CMake expects all listed sources to be present even if they have no code yet.

help me fix undefined reference linker errors in CMake

✔️ Awesome, I've got everything!

Great. Your store compiles and returns the correct value. All four files are saved.

ⓧ I'd like to double check the full code

// Manual KVStore test
#include "kvstore.hpp"
#include <iostream>
int main() {
    KVStore store;
    store.set("hello", "world");
    auto v = store.get("hello");
    std::cout << (v.has_value() ? v.value() : "NOT_FOUND") << "\n";
    return 0;
}

Your data layer is solid. The store handles concurrent reads and writes safely, and the protocol parser translates text commands into store operations. Next up, you'll build the thread pool that dispatches client connections to worker threads.

Build the Thread Pool

Your KVStore and Protocol are working together. You can parse commands and read/write to a thread-safe store. The problem is that a TCP server handling one client at a time is useless in practice.

To handle multiple clients simultaneously, you need a thread pool. Instead of spawning a new thread for every connection (expensive), a thread pool pre-creates a fixed number of worker threads that sit idle until work arrives. In this step, you will build that pool from scratch using a task queue, a mutex, and a condition variable.

In this step, get ready to:
  • Declare the ThreadPool class with its synchronization primitives.
  • Implement worker threads that wait on a condition variable and execute queued tasks.
  • Implement graceful shutdown so threads exit cleanly when the pool is destroyed.
Implement the thread pool class header

The header declares what the ThreadPool owns and exposes. It needs a vector of worker threads, a queue of tasks, and synchronization primitives to coordinate between the thread that submits work and the threads that execute it.

  • Open src/thread_pool.hpp in VS Code and replace the empty skeleton with this class declaration:
#pragma once

#include <vector>
#include <thread>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <atomic>

class ThreadPool {
public:
    explicit ThreadPool(std::size_t num_threads);
    ~ThreadPool();

    void submit(std::function<void()> task);

    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;

private:
    std::vector<std::thread> workers_;
    std::queue<std::function<void()>> tasks_;
    std::mutex mutex_;
    std::condition_variable cv_;
    std::atomic<bool> stop_{false};
};

What does this class declare?

  • workers_ holds the pre-created threads that sit in a loop waiting for tasks.
  • tasks_ is a FIFO queue of callable work items. Any function with no arguments and no return value can be submitted.
  • mutex_ and cv_ coordinate access to the queue. Workers sleep on the condition variable until a task is pushed.
  • stop_ is an atomic flag that tells workers to exit their loop during shutdown.
  • The deleted copy constructor and assignment operator prevent accidental duplication of a pool that owns running threads.
  • Save src/thread_pool.hpp.

You won't see a change yet. The next chunk implements the methods declared here.

Why std::function<void()> for tasks?

A std::function<void()> can wrap any callable with no arguments and no return value. Since the server will submit lambdas that capture the client socket file descriptor, this signature covers everything the pool needs to handle.

✔️ Awesome, I've got everything!

Great. Make sure you saved the file before moving on.

ⓧ I'd like to double check the full code

#pragma once

#include <vector>
#include <thread>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <atomic>

class ThreadPool {
public:
    explicit ThreadPool(std::size_t num_threads);
    ~ThreadPool();

    void submit(std::function<void()> task);

    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;

private:
    std::vector<std::thread> workers_;
    std::queue<std::function<void()>> tasks_;
    std::mutex mutex_;
    std::condition_variable cv_;
    std::atomic<bool> stop_{false};
};
Implement the worker loop

The constructor is where workers come to life. Each worker thread runs an infinite loop: lock the mutex, sleep on the condition variable until there is work or a stop signal, then dequeue and execute.

  • Open src/thread_pool.cpp and replace the empty skeleton with the constructor implementation:
#include "thread_pool.hpp"

ThreadPool::ThreadPool(std::size_t num_threads) {
    for (std::size_t i = 0; i < num_threads; ++i) {
        workers_.emplace_back([this] {
            while (true) {
                std::function<void()> task;
                {
                    std::unique_lock lock(mutex_);
                    cv_.wait(lock, [this] {
                        return stop_.load() || !tasks_.empty();
                    });
                    if (stop_.load() && tasks_.empty()) {
                        return;
                    }
                    task = std::move(tasks_.front());
                    tasks_.pop();
                }
                task();
            }
        });
    }
}

What does this constructor do?

  • It spawns num_threads workers, each running an infinite while (true) loop.
  • cv_.wait(lock, predicate) puts the thread to sleep until either stop_ is true or a task is available. This avoids busy-waiting and wasting CPU cycles.
  • The inner braces around the lock create a limited scope. The mutex is released before task() executes, so other workers can grab tasks concurrently.
  • The if (stop_.load() && tasks_.empty()) check ensures workers finish remaining tasks before exiting during shutdown.

You won't see a change yet. The next chunk adds the destructor and submit method to complete the file.

  • Add the destructor and submit() method below the constructor in src/thread_pool.cpp:
ThreadPool::~ThreadPool() {
    stop_.store(true);
    cv_.notify_all();
    for (auto& worker : workers_) {
        if (worker.joinable()) {
            worker.join();
        }
    }
}

void ThreadPool::submit(std::function<void()> task) {
    {
        std::unique_lock lock(mutex_);
        tasks_.push(std::move(task));
    }
    cv_.notify_one();
}

What do these methods do?

The destructor handles graceful shutdown. It sets stop_ to true, wakes all sleeping workers with notify_all(), and then joins each thread. This guarantees no worker is left dangling when the pool goes out of scope.

submit() locks the mutex, pushes the task onto the queue, then calls notify_one() to wake exactly one sleeping worker. This is an efficient producer-consumer pattern.

  • Save src/thread_pool.cpp.
  • Rebuild the project to verify compilation by running:
cmake --build build
  • You should see the build complete with no errors. The binary still runs the KVStore test from main, but the ThreadPool code compiles cleanly alongside it.

Seeing a compilation error?

  • If you see an error about an incomplete type or undeclared identifier, make sure #include "thread_pool.hpp" is the first line in src/thread_pool.cpp.
  • If you see a linker error about duplicate symbols, check you have not accidentally pasted the constructor twice.
  • If std::atomic is not found, confirm your header includes <atomic> in thread_pool.hpp.

Why won't my thread pool compile?

✔️ Awesome, I've got everything!

Great. Double check you saved both files before moving on.

ⓧ I'd like to double check the full code

#pragma once

#include <vector>
#include <thread>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <atomic>

class ThreadPool {
public:
    explicit ThreadPool(std::size_t num_threads);
    ~ThreadPool();

    void submit(std::function<void()> task);

    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;

private:
    std::vector<std::thread> workers_;
    std::queue<std::function<void()>> tasks_;
    std::mutex mutex_;
    std::condition_variable cv_;
    std::atomic<bool> stop_{false};
};
#include "thread_pool.hpp"

ThreadPool::ThreadPool(std::size_t num_threads) {
    for (std::size_t i = 0; i < num_threads; ++i) {
        workers_.emplace_back([this] {
            while (true) {
                std::function<void()> task;
                {
                    std::unique_lock lock(mutex_);
                    cv_.wait(lock, [this] {
                        return stop_.load() || !tasks_.empty();
                    });
                    if (stop_.load() && tasks_.empty()) {
                        return;
                    }
                    task = std::move(tasks_.front());
                    tasks_.pop();
                }
                task();
            }
        });
    }
}

ThreadPool::~ThreadPool() {
    stop_.store(true);
    cv_.notify_all();
    for (auto& worker : workers_) {
        if (worker.joinable()) {
            worker.join();
        }
    }
}

void ThreadPool::submit(std::function<void()> task) {
    {
        std::unique_lock lock(mutex_);
        tasks_.push(std::move(task));
    }
    cv_.notify_one();
}

Your thread pool is ready to accept work. Next up, you will build the TCP server that listens for client connections and dispatches each one to this pool as a task.

Build the TCP Server

You have a thread-safe key-value store, a thread pool ready to dispatch work, and a protocol parser that turns text commands into store operations. The missing piece is the network layer that accepts incoming client connections and feeds data through that pipeline.

In this step, you will build the TCP server using raw POSIX sockets. The server creates a listening socket, binds it to 127.0.0.1, and loops on accept() to hand each new connection to a thread pool worker.

In this step, get ready to:
  • Create a listening TCP socket bound to localhost.
  • Implement the accept loop and per-client handler.
  • Wire the server, store, and thread pool together in main with graceful shutdown.
Create the listening socket

Every TCP server follows the same four-step ritual: create a socket, set options, bind to an address, and start listening. You will declare the Server class interface first, then implement the full socket lifecycle.

  • In VS Code, open src/server.hpp (currently an empty skeleton) and replace its contents with this class declaration:
#pragma once

#include <cstdint>
#include <string>
#include <atomic>
#include "kvstore.hpp"
#include "thread_pool.hpp"

class Server {
public:
    Server(KVStore& store, ThreadPool& pool, uint16_t port);
    ~Server();

    void run();
    void stop();

private:
    void handle_client(int client_fd);

    KVStore& store_;
    ThreadPool& pool_;
    uint16_t port_;
    int listen_fd_{-1};
    std::atomic<bool> running_{false};
};

What does this header declare?

  • The Server holds references to a KVStore and a ThreadPool so it can share them with every client handler.
  • listen_fd_ is the file descriptor for the listening socket. Initialized to -1 (invalid) until run() creates it.
  • running_ is an atomic flag that the accept loop checks on every iteration. Calling stop() flips it to false and closes the socket, breaking the loop.
  • handle_client() is private because only the thread pool calls it. Each accepted connection gets its own invocation on a worker thread.
  • Save src/server.hpp.

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file before moving on.

ⓧ I'd like to double check the full code

#pragma once

#include <cstdint>
#include <string>
#include <atomic>
#include "kvstore.hpp"
#include "thread_pool.hpp"

class Server {
public:
    Server(KVStore& store, ThreadPool& pool, uint16_t port);
    ~Server();

    void run();
    void stop();

private:
    void handle_client(int client_fd);

    KVStore& store_;
    ThreadPool& pool_;
    uint16_t port_;
    int listen_fd_{-1};
    std::atomic<bool> running_{false};
};

Now implement the server core. The run() method walks through the full POSIX socket ceremony and then enters the accept loop.

  • Open src/server.cpp and replace its contents with the following server lifecycle and socket logic:
#include "server.hpp"
#include "protocol.hpp"

#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

Server::Server(KVStore& store, ThreadPool& pool, uint16_t port)
    : store_(store), pool_(pool), port_(port) {}

Server::~Server() {
    stop();
}

void Server::run() {
    // Create a TCP socket
    listen_fd_ = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd_ < 0) {
        std::cerr << "Failed to create socket: " << std::strerror(errno) << "\n";
        return;
    }

    // Allow immediate port reuse after restart
    int opt = 1;
    if (setsockopt(listen_fd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        std::cerr << "Failed to set SO_REUSEADDR: " << std::strerror(errno) << "\n";
        close(listen_fd_);
        return;
    }

    // Bind to 127.0.0.1 on the configured port
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    addr.sin_port = htons(port_);

    if (bind(listen_fd_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
        std::cerr << "Failed to bind: " << std::strerror(errno) << "\n";
        close(listen_fd_);
        return;
    }

    // Start listening for incoming connections
    if (listen(listen_fd_, SOMAXCONN) < 0) {
        std::cerr << "Failed to listen: " << std::strerror(errno) << "\n";
        close(listen_fd_);
        return;
    }

    running_.store(true);
    std::cout << "Listening on 127.0.0.1:" << port_ << "\n";

    // Accept loop: dispatch each new connection to the thread pool
    while (running_.load()) {
        sockaddr_in client_addr{};
        socklen_t client_len = sizeof(client_addr);
        int client_fd = accept(listen_fd_, reinterpret_cast<sockaddr*>(&client_addr), &client_len);

        if (client_fd < 0) {
            if (running_.load()) {
                std::cerr << "Accept failed: " << std::strerror(errno) << "\n";
            }
            continue;
        }

        pool_.submit([this, client_fd] {
            handle_client(client_fd);
        });
    }
}

void Server::stop() {
    running_.store(false);
    if (listen_fd_ >= 0) {
        close(listen_fd_);
        listen_fd_ = -1;
    }
}

What does this code do?

The run() method follows the standard POSIX socket sequence:

  • socket(AF_INET, SOCK_STREAM, 0) creates a TCP/IPv4 socket and returns a file descriptor.
  • setsockopt(..., SO_REUSEADDR, ...) lets you restart the server immediately without waiting for the OS to release the port.
  • bind() attaches the socket to 127.0.0.1 on the chosen port. INADDR_LOOPBACK ensures only local connections are accepted.
  • listen() marks the socket as passive (ready to receive connections). SOMAXCONN sets the backlog to the OS maximum.
  • The while (running_.load()) loop blocks on accept() until a client connects, then submits a handle_client task to the thread pool.
  • stop() flips the flag and closes the listening socket, which unblocks accept() with an error, ending the loop.

Why SO_REUSEADDR?

When a TCP server shuts down, the OS keeps the port in a TIME_WAIT state for up to 60 seconds. Without SO_REUSEADDR, restarting the server during that window fails with a "bind: Address already in use" error. This option tells the OS to allow immediate rebinding.

  • Save src/server.cpp. The file is not yet complete (handle_client is still missing), so do not try to build yet.
Implement the accept loop and client handler

Each time accept() returns a client file descriptor, the thread pool submits a call to handle_client. This function is where your protocol parser meets the network. It reads bytes from the socket, assembles complete lines, and dispatches each one.

  • In src/server.cpp, add the handle_client method at the bottom of the file, below Server::stop():
void Server::handle_client(int client_fd) {
    Protocol protocol(store_);
    std::string buffer;
    char chunk[1024];

    while (true) {
        // Read raw bytes from the client socket
        ssize_t bytes_read = recv(client_fd, chunk, sizeof(chunk) - 1, 0);
        if (bytes_read <= 0) {
            break;
        }
        chunk[bytes_read] = '\0';
        buffer += chunk;

        // Process complete lines (commands end with newline)
        std::size_t pos;
        while ((pos = buffer.find('\n')) != std::string::npos) {
            std::string line = buffer.substr(0, pos);
            buffer = buffer.substr(pos + 1);

            // Remove trailing carriage return if present
            if (!line.empty() && line.back() == '\r') {
                line.pop_back();
            }

            if (line.empty()) {
                continue;
            }

            std::string response = protocol.execute(line);
            send(client_fd, response.c_str(), response.size(), 0);
        }
    }

    close(client_fd);
}

What does this code do?

  • recv() reads up to 1023 bytes at a time. TCP is a stream protocol, so one recv call might return half a command or two commands glued together.
  • The buffer accumulates bytes until a newline (\n) appears. Each complete line is one command.
  • Carriage return (\r) stripping handles clients that send \r\n line endings (like Windows telnet).
  • protocol.execute(line) parses the command and returns the response string, which send() writes back to the client.
  • When recv() returns 0, the client disconnected. The handler breaks out and closes the socket.
  • Save src/server.cpp.

Seeing a compile error about handle_client?

  • Make sure the method signature matches exactly: void Server::handle_client(int client_fd). A mismatch with the header declaration causes a linker error.
  • Confirm you included #include "protocol.hpp" at the top of server.cpp. Without it, the Protocol class is undefined.

Help me debug a compile error in server.cpp

✔️ Awesome, I've got everything!

Great. Double check you have saved src/server.cpp before moving on.

ⓧ I'd like to double check the full code

#include "server.hpp"
#include "protocol.hpp"

#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

Server::Server(KVStore& store, ThreadPool& pool, uint16_t port)
    : store_(store), pool_(pool), port_(port) {}

Server::~Server() {
    stop();
}

void Server::run() {
    listen_fd_ = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd_ < 0) {
        std::cerr << "Failed to create socket: " << std::strerror(errno) << "\n";
        return;
    }

    int opt = 1;
    if (setsockopt(listen_fd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        std::cerr << "Failed to set SO_REUSEADDR: " << std::strerror(errno) << "\n";
        close(listen_fd_);
        return;
    }

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    addr.sin_port = htons(port_);

    if (bind(listen_fd_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
        std::cerr << "Failed to bind: " << std::strerror(errno) << "\n";
        close(listen_fd_);
        return;
    }

    if (listen(listen_fd_, SOMAXCONN) < 0) {
        std::cerr << "Failed to listen: " << std::strerror(errno) << "\n";
        close(listen_fd_);
        return;
    }

    running_.store(true);
    std::cout << "Listening on 127.0.0.1:" << port_ << "\n";

    while (running_.load()) {
        sockaddr_in client_addr{};
        socklen_t client_len = sizeof(client_addr);
        int client_fd = accept(listen_fd_, reinterpret_cast<sockaddr*>(&client_addr), &client_len);

        if (client_fd < 0) {
            if (running_.load()) {
                std::cerr << "Accept failed: " << std::strerror(errno) << "\n";
            }
            continue;
        }

        pool_.submit([this, client_fd] {
            handle_client(client_fd);
        });
    }
}

void Server::stop() {
    running_.store(false);
    if (listen_fd_ >= 0) {
        close(listen_fd_);
        listen_fd_ = -1;
    }
}

void Server::handle_client(int client_fd) {
    Protocol protocol(store_);
    std::string buffer;
    char chunk[1024];

    while (true) {
        ssize_t bytes_read = recv(client_fd, chunk, sizeof(chunk) - 1, 0);
        if (bytes_read <= 0) {
            break;
        }
        chunk[bytes_read] = '\0';
        buffer += chunk;

        std::size_t pos;
        while ((pos = buffer.find('\n')) != std::string::npos) {
            std::string line = buffer.substr(0, pos);
            buffer = buffer.substr(pos + 1);

            if (!line.empty() && line.back() == '\r') {
                line.pop_back();
            }

            if (line.empty()) {
                continue;
            }

            std::string response = protocol.execute(line);
            send(client_fd, response.c_str(), response.size(), 0);
        }
    }

    close(client_fd);
}
Wire everything together in main.cpp

The server needs an entry point that instantiates all the components, starts listening, and handles graceful shutdown when you press Ctrl+C. You will replace the manual KVStore test with the real server startup code.

  • Open src/main.cpp and replace its entire contents with the following:
#include "server.hpp"
#include "kvstore.hpp"
#include "thread_pool.hpp"

#include <iostream>
#include <csignal>
#include <cstdlib>

namespace {
    std::atomic<bool> g_shutdown_requested{false};
}

void signal_handler(int) {
    g_shutdown_requested.store(true);
}

int main(int argc, char* argv[]) {
    // Default port, overridable via command-line argument
    uint16_t port = 7878;
    if (argc > 1) {
        port = static_cast<uint16_t>(std::atoi(argv[1]));
    }

    // Register signal handlers for graceful shutdown
    std::signal(SIGINT, signal_handler);
    std::signal(SIGTERM, signal_handler);

    // Instantiate the three core components
    KVStore store;
    ThreadPool pool(4);
    Server server(store, pool, port);

    std::cout << "Starting kvserver on port " << port << "...\n";
    std::cout << "Press Ctrl+C to stop.\n";

    // Run server in a separate thread so main can poll for shutdown
    std::thread server_thread([&server] {
        server.run();
    });

    while (!g_shutdown_requested.load()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    std::cout << "\nShutting down...\n";
    server.stop();

    if (server_thread.joinable()) {
        server_thread.join();
    }

    std::cout << "Server stopped.\n";
    return 0;
}

What does this code do?

  • g_shutdown_requested is an anonymous-namespace atomic flag. The signal handler sets it to true when you press Ctrl+C (SIGINT) or the process receives SIGTERM.
  • The server runs in its own thread because run() blocks inside accept(). The main thread polls the shutdown flag every 100ms.
  • server.stop() closes the listening socket, which causes accept() to return with an error. The server thread then exits its loop and the main thread joins it cleanly.
  • ThreadPool(4) creates 4 worker threads. This means up to 4 clients can be served simultaneously.
  • Save src/main.cpp.

✔️ Awesome, I've got everything!

Great. Make sure you have saved all three files before building.

ⓧ I'd like to double check the full code

#include "server.hpp"
#include "kvstore.hpp"
#include "thread_pool.hpp"

#include <iostream>
#include <csignal>
#include <cstdlib>

namespace {
    std::atomic<bool> g_shutdown_requested{false};
}

void signal_handler(int) {
    g_shutdown_requested.store(true);
}

int main(int argc, char* argv[]) {
    uint16_t port = 7878;
    if (argc > 1) {
        port = static_cast<uint16_t>(std::atoi(argv[1]));
    }

    std::signal(SIGINT, signal_handler);
    std::signal(SIGTERM, signal_handler);

    KVStore store;
    ThreadPool pool(4);
    Server server(store, pool, port);

    std::cout << "Starting kvserver on port " << port << "...\n";
    std::cout << "Press Ctrl+C to stop.\n";

    std::thread server_thread([&server] {
        server.run();
    });

    while (!g_shutdown_requested.load()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    std::cout << "\nShutting down...\n";
    server.stop();

    if (server_thread.joinable()) {
        server_thread.join();
    }

    std::cout << "Server stopped.\n";
    return 0;
}
  • Build the project by running this command in the terminal from your kvserver project directory:
cmake --build build
  • Confirm the build succeeds with no errors. You should see the compiler process each .cpp file and produce the build/kvserver binary.

Build failing?

  • If you see "undefined reference" errors for Server methods, make sure server.cpp has all four methods: constructor, destructor, run(), stop(), and handle_client().
  • If you see errors about missing headers like sys/socket.h, confirm you are on macOS with Xcode Command Line Tools installed. These headers ship by default.
  • If CMake cannot find the build directory, re-run cmake -B build first to regenerate it.

Help me debug my kvserver build failure

  • Verify the binary exists by running:
ls build/kvserver

You should see build/kvserver listed in the output. The binary is ready to run.

Your server is compiled and ready. In the next step, you will start it up and test it end-to-end using nc (netcat) to send live commands and verify concurrent access across multiple clients.

Test the Server End-to-End

You have a complete TCP server with a thread pool, protocol parser, and thread-safe store. All the pieces are wired together in main.cpp.

Now it is time to prove it actually works. You will spin up the server, connect to it with nc (netcat), and fire commands at it from multiple sessions simultaneously to verify that the concurrency holds up under real multi-client usage.

In this step, get ready to:
  • Build and run the server binary.
  • Test all protocol commands interactively with netcat.
  • Verify concurrent access from multiple clients.
Build and run the server

Even though the binary was compiled in a previous step, it is good practice to rebuild before testing. This ensures any last-minute edits are picked up.

  • Rebuild the project by running this command in your terminal:
cmake --build build
  • You should see the compilation output complete with no errors.
  • Start the server by running:
./build/kvserver

You should see this output in your terminal:

Starting kvserver on port 7878...
Press Ctrl+C to stop.
Listening on 127.0.0.1:7878

What do these messages mean?

The server is now blocking on accept(), waiting for incoming TCP connections on port 7878. It will stay running in this terminal until you press Ctrl+C.

Leave this terminal running. You will open a second terminal to connect as a client.

Seeing "Failed to bind" or "Address already in use"?

  • Another process is using port 7878. Stop it or run the server on a different port by passing a port number as an argument (e.g. ./build/kvserver 9090).
  • If you recently stopped the server and restarted it too quickly, the OS may still hold the socket. Wait a few seconds and try again.

Help me debug a socket bind failure on port 7878.

Test with netcat from a separate terminal

Netcat is a simple networking utility that ships with macOS. It can open a raw TCP connection to any host and port, making it perfect for testing a text-based protocol interactively.

  • Open a new terminal window. Keep the server running in the first terminal.
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open a second terminal.
  • Connect to the server by running:
nc localhost 7878

Your cursor drops to a blank line. This means netcat connected successfully and is waiting for you to type commands.

  • Type SET name Alice and press Enter.

You should see OK printed back immediately.

  • Type GET name and press Enter.

You should see Alice returned.

  • Type EXISTS name and press Enter.

You should see 1 (meaning the key exists).

  • Type DEL name and press Enter.

You should see 1 (one key was deleted).

  • Type KEYS and press Enter.

You should see (empty) since you deleted the only key.

Why is this so fast?

Every command round-trips over a TCP connection on localhost. The server receives your line, parses it, queries the in-memory store, and sends the response. Because there is no disk I/O or network latency, responses appear instant.

This is the same architecture behind systems like Redis. The speed comes from keeping everything in RAM and using an efficient event-driven loop.

Not getting any response?

  • Make sure the server is still running in the first terminal. If you accidentally closed it, restart it with ./build/kvserver.
  • Check that you pressed Enter after each command. Netcat sends data line-by-line.
  • If netcat printed Connection refused, the server is not running or you typed the wrong port.

Help me troubleshoot my netcat connection to kvserver.

Test concurrent access

A real server handles multiple clients at once. Your thread pool dispatches each connection to a worker thread, and the std::shared_mutex protects the store from data races. Time to prove it.

  • Keep your existing netcat session open (or reconnect if you closed it).
  • Open a third terminal window using the same method as before.
  • Connect a second netcat client by running:
nc localhost 7878

You now have two independent clients connected to the same server.

  • In Client 1 (your first netcat session), type SET color blue and press Enter.
  • In Client 2 (your second netcat session), type SET city Tokyo and press Enter.
  • In Client 1, type GET city and press Enter.

You should see Tokyo returned. Client 1 can read a key that Client 2 wrote. The shared store is working across threads.

  • In Client 2, type KEYS and press Enter.

You should see both color and city listed, confirming that both clients share a single data store.

What makes this thread-safe?

Each client connection runs on its own thread from the pool. When two threads call get() simultaneously, they both acquire a shared lock and read in parallel. When a thread calls set(), it acquires an exclusive lock, blocking all other readers and writers until the write completes.

This reader-writer pattern is exactly how production caches handle high-concurrency workloads. Multiple reads proceed without contention while writes get exclusive access.

  • Disconnect both netcat sessions by pressing Ctrl+C in each client terminal.
  • Switch to the server terminal and press Ctrl+C to stop the server.

You should see the shutdown messages:

Shutting down...
Server stopped.

Server hangs on shutdown?

  • Make sure you disconnected all netcat clients first. If a client is still connected, the server may be blocked in a recv() call on that thread.
  • If the server does not stop after a few seconds, press Ctrl+C again or close the terminal window.

Help me debug a server that hangs on shutdown.

Your server is fully operational. It accepts connections, handles commands, and serves multiple clients concurrently without crashing or corrupting data. Next up, the Secret Mission challenges you to add TTL expiration so keys can auto-delete after a timeout.

Secret mission

Add TTL Expiration

Your key-value server stores data forever. Real caches like Redis let keys expire after a set time. Can you add time-to-live (TTL) support so keys automatically vanish after a configurable number of seconds?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources on disk, pause the running server, or delete everything entirely.

Resources you used:

  • The kvserver project directory on your Desktop (source files, CMake config, and compiled binary).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with the server, adding new commands, or extending the protocol.

  • Your compiled binary stays in build/kvserver ready to run whenever you want.
  • No cloud resources or background processes are running. Nothing accumulates cost.

Pause - I'll come back to this later

Stop the server process to free up the port. Your source files and compiled binary remain on disk for later use.

  • If the server is still running in your terminal, press Ctrl+C to stop it.
  • Your project files stay in ~/Desktop/kvserver. When you come back, rebuild with cmake --build build and run ./build/kvserver to start the server again.

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes your source code, build artifacts, and the compiled binary.

  • If the server is still running, press Ctrl+C to stop it first.
  • Delete the entire project directory by running:
rm -rf ~/Desktop/kvserver
  • Confirm the folder is gone by running:
ls ~/Desktop/kvserver

You should see a "No such file or directory" message confirming the deletion.

Nice Work!

Nice Work!

You just built a production-style concurrent TCP key-value server from scratch using raw POSIX sockets and modern C++20. This is the exact architecture that powers systems like Redis and Memcached.

You learned how to:

  • Built a TCP server from raw POSIX sockets with a thread pool dispatching concurrent client connections to worker threads.
  • Designed a text-based wire protocol supporting SET, GET, DEL, EXISTS, and KEYS commands, testable end-to-end with netcat.
  • Created a thread-safe in-memory key-value store using std::shared_mutex for high-performance reader-writer concurrency.
  • Secret Mission: Added TTL expiration with lazy cleanup, enabling time-based key eviction.

Ready to quiz yourself?