A portfolio-quality mini Redis-style in-memory key-value server written in C++17.
This project implements a TCP database server with Redis-inspired commands, RESP request/response support, lazy key expiration, optional append-only persistence, and basic multi-client concurrency. It is intentionally smaller than Redis, but it is built to demonstrate real backend and systems programming concepts in clean, beginner-friendly C++.
- TCP socket programming with
socket,bind,listen,accept,recv, andsend - Protocol parsing for both simple text commands and a RESP array subset
- RESP-style response formatting
- Command dispatch separated from networking
- In-memory storage with
std::unordered_map - Lazy expiration using
std::chrono - Append-only file persistence and replay
- Multi-client handling with
std::thread - Shared-state protection with
std::mutexandstd::scoped_lock - CMake-based C++17 project organization
- Lightweight unit tests using
assertand CTest
- Configurable TCP port with
--port - Optional append-only persistence with
--aof - Simple manual command input:
SET name Slava
GET name
- RESP array request input:
*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$5\r\nSlava\r\n
- RESP-style responses:
+OK\r\n
$5\r\nSlava\r\n
:1\r\n
$-1\r\n
- One thread per connected client
- Thread-safe key-value store and append-only log
- Unit tests for parser, command handling, storage, expiration, persistence, and concurrent increments
| Command | Arguments | Response | Description |
|---|---|---|---|
PING |
none | +PONG\r\n |
Health check |
SET |
key value |
+OK\r\n |
Stores a string value |
GET |
key |
bulk string or $-1\r\n |
Reads a value |
DEL |
key |
integer | Deletes a key, returns 1 or 0 |
EXISTS |
key |
integer | Checks whether a key exists |
INCR |
key |
integer or error | Increments an integer value |
EXPIRE |
key seconds |
integer | Adds a relative expiration |
TTL |
key |
integer | Returns remaining TTL, -1, or -2 |
.
├── CMakeLists.txt
├── include/
│ ├── aof_log.hpp
│ ├── command.hpp
│ ├── command_handler.hpp
│ ├── command_parser.hpp
│ ├── key_value_store.hpp
│ ├── resp_response.hpp
│ └── tcp_server.hpp
├── src/
│ ├── aof_log.cpp
│ ├── command_handler.cpp
│ ├── command_parser.cpp
│ ├── key_value_store.cpp
│ ├── main.cpp
│ ├── resp_response.cpp
│ └── tcp_server.cpp
├── tests/
│ ├── test_aof_log.cpp
│ ├── test_command_handler.cpp
│ ├── test_command_parser.cpp
│ └── test_key_value_store.cpp
└── docs/
└── architecture.md
On this Windows setup with MinGW:
cmake -S . -B build -G Ninja -DCMAKE_CXX_COMPILER=C:/mingw-w64/mingw64/bin/g++.exe
cmake --build buildOn another machine with a configured C++17 compiler, this may be enough:
cmake -S . -B build
cmake --build buildWithout persistence:
.\build\miniredis.exe --port 6379With append-only persistence:
.\build\miniredis.exe --port 6379 --aof data.aofStart the server in one PowerShell window:
.\build\miniredis.exe --port 6379 --aof data.aofOpen a client in another PowerShell window:
$client = [System.Net.Sockets.TcpClient]::new("127.0.0.1", 6379)
$stream = $client.GetStream()
$buffer = New-Object byte[] 1024Send simple text commands:
$cmd = "PING`r`nSET name Slava`r`nGET name`r`nEXISTS name`r`nDEL name`r`nGET name`r`n"
$bytes = [Text.Encoding]::ASCII.GetBytes($cmd)
$stream.Write($bytes, 0, $bytes.Length)
$count = $stream.Read($buffer, 0, $buffer.Length)
[Text.Encoding]::ASCII.GetString($buffer, 0, $count)Expected response shape:
+PONG
+OK
$5
Slava
:1
:1
$-1
Close the client:
$client.Close()The server also accepts RESP arrays of bulk strings. This sends SET name Slava:
$client = [System.Net.Sockets.TcpClient]::new("127.0.0.1", 6379)
$stream = $client.GetStream()
$cmd = "*3`r`n`$3`r`nSET`r`n`$4`r`nname`r`n`$5`r`nSlava`r`n"
$bytes = [Text.Encoding]::ASCII.GetBytes($cmd)
$stream.Write($bytes, 0, $bytes.Length)
$buffer = New-Object byte[] 1024
$count = $stream.Read($buffer, 0, $buffer.Length)
[Text.Encoding]::ASCII.GetString($buffer, 0, $count)
$client.Close()Expected:
+OK
Run with persistence:
.\build\miniredis.exe --port 6379 --aof data.aofSend writes:
SET name Slava
INCR count
INCR count
EXPIRE name 60
The file will contain simple line commands:
SET name Slava
INCR count
INCR count
EXPIRE name 60
On restart, the server reads the file and replays the commands to rebuild the in-memory store.
TCP client
-> TcpServer receives bytes
-> CommandParser parses simple text or RESP input
-> CommandHandler validates and executes commands
-> KeyValueStore stores values and expiration metadata
-> AofLog optionally persists successful mutations
-> RespResponse formats output
-> TcpServer sends bytes back to the client
See docs/architecture.md for a fuller explanation.
TcpServeraccepts a client socket.- A client thread reads bytes with
recv. - Bytes are appended to a per-client pending buffer.
CommandParser::try_parsechecks whether a full command is available.- The parser returns a
Command. CommandHandlervalidates argument counts and callsKeyValueStore.- Successful mutating commands are appended to
AofLogwhen persistence is enabled. RespResponsecreates a RESP wire response.TcpServerwrites the response withsend.
The server uses one std::thread per connected client. TcpServer owns those threads in a std::vector<std::thread> and joins joinable threads during stop().
Shared state is protected:
KeyValueStoreuses a mutex around map operations.AofLoguses a mutex around file append/load operations.TcpServeruses a mutex around the client thread vector.
This model is easy to understand and works well for learning. At very high concurrency, a thread per client can become expensive because every connection consumes OS thread resources.
Build and run tests:
cmake --build build
ctest --test-dir build --output-on-failureThe tests cover:
- Simple command parsing
- RESP request parsing
- Command handling
- Store operations
- Expiration behavior
- AOF append and replay
- Basic concurrent increments
- This is not full Redis.
- Only a small command set is implemented.
- Input supports simple line commands plus a RESP array-of-bulk-strings subset.
- Values are strings only.
- One-thread-per-client is simple but expensive at high concurrency.
- Shutdown is improved, but not a perfect production graceful shutdown.
- AOF grows forever because there is no compaction or rewrite.
EXPIREreplay uses a relative TTL from startup, not the original absolute expiration time.- No authentication.
- No clustering.
- No replication.
- No transactions.
- No pub/sub.
- No eviction policy.
- No snapshots.
- AOF compaction/rewrite
- Snapshot persistence
- Absolute expiration timestamps in persistence
- Thread pool instead of one thread per client
- Event loop and non-blocking I/O
- More Redis commands and data types
- Better graceful shutdown with signal handling and client socket tracking
- Benchmarks and load tests
- More robust integration tests with real TCP clients
Suggested repository name:
mini-redis-cpp
Suggested GitHub About text:
A C++17 Redis-inspired TCP key-value server with RESP parsing, TTLs, AOF persistence, and multi-client concurrency.
Suggested resume bullet points:
- Built a Redis-inspired in-memory key-value database in C++17 with TCP sockets, RESP parsing, TTL expiration, and append-only persistence.
- Implemented thread-safe multi-client command handling using
std::thread,std::mutex, and clean separation between networking, parsing, storage, and persistence. - Added CMake build automation and unit tests covering parser behavior, command execution, expiration, persistence replay, and concurrent increments.
Suggested portfolio description:
Mini Redis C++17 is a systems/backend project that implements a Redis-style TCP key-value server from scratch. It supports simple text and RESP command input, RESP output, core string commands, lazy expiration, append-only persistence, startup replay, and multi-client access with mutex-protected shared state. The project emphasizes clean architecture, testability, and readable C++17.