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.
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.
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
GlobalThreadPoolof N worker threads. Workers grab tasks from astd::queue<std::function<void()>>guarded by a singlestd::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 sockets3// 2. accept() new connections4// 3. for any client with bytes ready, hand the work to the5// worker pool instead of executing inline6GlobalThreadPool::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 });03 · Command pipeline
Open/closed: each command is a class, dispatched by a factory
Inside a worker the buffered text turns into a concrete
ICommandinstance via aCommandFactory. There are 40-odd concrete classes —SETCommand,GETCommand,LPUSHCommand,HGETALLCommand,ZRANGECommand,XADDCommand— each in its own header underinclude/Database/Commands/ConcreteCommands/.Adding a new command is one new header that derives from
ICommandplus one registration line. The processor, the wire protocol, the persistence layer stay closed for modification.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 anIValueinterface. 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
LPUSHon a key whose value is a string fails the dynamic-cast and returns aWRONGTYPEerror; a brand-new key constructs aListValuevia 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 all3// 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};05 · Cross-cutting concerns
TTL is a decorator. Persistence is an observer.
Time-to-live could have been a field on
Recordchecked everywhere keys are touched. Instead it's aTTLDatabaseDecoratorthat wraps the database; the innerDatabasedoesn'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.
SnapshotPersistenceandAOFPersistenceboth implementIDatabaseObserverand register with the database. Every mutation triggersonDatabaseModified(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 sameIValue::serialize()interface used on the wire.●include/Database/Persistence/Implementations/AOFPersistence.hcpp 1void onDatabaseModified(2 const std::string& operation,3 const std::string& recordId4) override {5 std::lock_guard<std::mutex> lock(writeMutex);6 if (aofStream.is_open()) {7 aofStream << operation << " " << recordId << "\n";8 aofStream.flush();9 }10}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 forredis-benchmarksingle-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.
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 100000and watch the latencies.
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.
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
GlobalThreadPoolof N worker threads. Workers grab tasks from astd::queue<std::function<void()>>guarded by a singlestd::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 sockets3// 2. accept() new connections4// 3. for any client with bytes ready, hand the work to the5// worker pool instead of executing inline6GlobalThreadPool::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 });03 · Command pipeline
Open/closed: each command is a class, dispatched by a factory
Inside a worker the buffered text turns into a concrete
ICommandinstance via aCommandFactory. There are 40-odd concrete classes —SETCommand,GETCommand,LPUSHCommand,HGETALLCommand,ZRANGECommand,XADDCommand— each in its own header underinclude/Database/Commands/ConcreteCommands/.Adding a new command is one new header that derives from
ICommandplus one registration line. The processor, the wire protocol, the persistence layer stay closed for modification.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 anIValueinterface. 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
LPUSHon a key whose value is a string fails the dynamic-cast and returns aWRONGTYPEerror; a brand-new key constructs aListValuevia 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 all3// 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};05 · Cross-cutting concerns
TTL is a decorator. Persistence is an observer.
Time-to-live could have been a field on
Recordchecked everywhere keys are touched. Instead it's aTTLDatabaseDecoratorthat wraps the database; the innerDatabasedoesn'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.
SnapshotPersistenceandAOFPersistenceboth implementIDatabaseObserverand register with the database. Every mutation triggersonDatabaseModified(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 sameIValue::serialize()interface used on the wire.●include/Database/Persistence/Implementations/AOFPersistence.hcpp 1void onDatabaseModified(2 const std::string& operation,3 const std::string& recordId4) override {5 std::lock_guard<std::mutex> lock(writeMutex);6 if (aofStream.is_open()) {7 aofStream << operation << " " << recordId << "\n";8 aofStream.flush();9 }10}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 forredis-benchmarksingle-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.
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 100000and watch the latencies.
Built with.
Senior backend and platform roles, hard systems problems, or an argument about thread pools.
Open a connectionRecruiter mode