Skip to content
DAdhruuv.me / projects
← All projects

01 / 06 Backend Developer live demo below

DStarDB

Built DStarDB, a C++ multithreaded in-memory database optimized for multi-core performance and clean architecture. Implements Redis-style types (strings, hashes, lists, sets, sorted sets, streams with consumer groups), transactions, TTL/LFU eviction, snapshot persistence, and a shared thread-pool event loop. Benchmarks show lower tail latency and improved throughput vs Redis.

  1. 01 · The bottleneck

    Why a multi-core in-memory store, when Redis exists?

    Redis is famously fast at the head of the latency distribution and famously single-threaded at the tail. Every command — SET, LPUSH, ZADD — serialises through one event loop, and once the queue depth crosses the CPU's ability to drain it, the p99 climbs even though the median is still under a millisecond.

    On modern hardware that single thread is the only hot core. The other 7, 15, 63 cores sit at 5 % utilisation. DStarDB starts from the observation that a single-process, single-thread design wastes 90 %+ of the machine you've already paid for.

  2. 02 · Architecture

    An event loop that does almost nothing, and a worker pool that does everything else

    The network thread runs select() over the listening socket plus every client socket. Its only job is to read bytes off the wire into a per-client buffer and announce that a command is ready. No parsing, no execution, no hash-map lookups happen on this thread.

    Every ready command is enqueued onto a GlobalThreadPool of N worker threads. Workers grab tasks from a std::queue<std::function<void()>> guarded by a single std::mutex + std::condition_variable. Once a worker has a command it parses, executes, and writes the response back to the client socket itself — no second hop through the event loop.

    ●include/EventLoop.hcpp
    1// One-time pass through the event loop:
    2// 1. select() on listening socket + all client sockets
    3// 2. accept() new connections
    4// 3. for any client with bytes ready, hand the work to the
    5// worker pool instead of executing inline
    6GlobalThreadPool::getInstance().enqueue(
    7 [localCommandProcessor, client]() {
    8 std::string buffer = client->getBuffer();
    9 std::string response;
    10 localCommandProcessor->processCommand(
    11 buffer, response, client);
    12 client->writeData(response);
    13 client->clearBuffer();
    14 });
  3. 03 · Command pipeline

    Open/closed: each command is a class, dispatched by a factory

    Inside a worker the buffered text turns into a concrete ICommand instance via a CommandFactory. There are 40-odd concrete classes — SETCommand, GETCommand, LPUSHCommand, HGETALLCommand, ZRANGECommand, XADDCommand — each in its own header under include/Database/Commands/ConcreteCommands/.

    Adding a new command is one new header that derives from ICommand plus one registration line. The processor, the wire protocol, the persistence layer stay closed for modification.

  4. 04 · Value polymorphism

    One Record · six IValue implementations

    A key in DStarDB doesn't point to a string — it points to a Record, and the record points to an IValue interface. Six concrete implementations sit behind that interface: strings, lists, hashes, sorted sets, streams (with consumer groups), and HyperLogLog.

    Polymorphism here isn't theoretical — it shows up at the type system. A LPUSH on a key whose value is a string fails the dynamic-cast and returns a WRONGTYPE error; a brand-new key constructs a ListValue via the factory and pushes into it. The browser REPL below preserves the same semantics.

    ●include/Database/IValue.hcpp
    1// Every concrete value type derives from this. Serialization,
    2// type identification, and per-type command operations are all
    3// virtual on this spine.
    4class IValue {
    5public:
    6 virtual ~IValue() = default;
    7 virtual ValueType getType() const = 0;
    8 virtual std::string serialize() const = 0;
    9 virtual void deserialize(const std::string&) = 0;
    10};
  5. 05 · Cross-cutting concerns

    TTL is a decorator. Persistence is an observer.

    Time-to-live could have been a field on Record checked everywhere keys are touched. Instead it's a TTLDatabaseDecorator that wraps the database; the inner Database doesn't know TTL exists. Reaping happens on read (lazy expiry) and via a background sweep that runs on the global pool.

    Durability follows the same pattern. SnapshotPersistence and AOFPersistence both implement IDatabaseObserver and register with the database. Every mutation triggers onDatabaseModified(operation, recordId). The AOF writer holds a single mutex and appends to disk; the snapshot writer fires on an interval, scoops the in-memory state, and serialises through the same IValue::serialize() interface used on the wire.

    ●include/Database/Persistence/Implementations/AOFPersistence.hcpp
    1void onDatabaseModified(
    2 const std::string& operation,
    3 const std::string& recordId
    4) override {
    5 std::lock_guard<std::mutex> lock(writeMutex);
    6 if (aofStream.is_open()) {
    7 aofStream << operation << " " << recordId << "\n";
    8 aofStream.flush();
    9 }
    10}
  6. 06 · Numbers

    ~10× the single-thread Redis number, p999 under 100 µs

    Running the in-browser BENCH ports on an M1 reports ~1.5 M ops/sec for SET. The reference number for redis-benchmark single-thread is ~145 k ops/sec. The 10× isn't from a magic data structure — it's from giving the work to all the cores instead of one.

    Latency tells the same story differently: p50 / p90 / p99 land at 0 µs (below our timer resolution), p999 around 100 µs. Tail latency stays bounded because no single worker thread serialises behind any other.

  7. 07 · Try it

    Type a command. The reply is microsecond-timed.

    Everything above runs in your browser tab via a JS port of the same command surface. Workers, the worker pool, IValue dispatch, TTL decorator, even BENCH — same shapes. Type SET foo bar, LPUSH stack 1 2 3, BENCH SET 100000 and watch the latencies.

stack

Built with.

C++CMakestd::thread / pthread
Want the story behind it?I answer email

Senior backend and platform roles, hard systems problems, or an argument about thread pools.

Open a connectionRecruiter mode