A Persistent Multithreaded In-Memory Key-Value Store in C++
A lightweight Redis-inspired TCP server built from scratch using C++17, POSIX sockets, multithreading, synchronization, snapshot persistence, Docker, and GitHub Actions.
- Overview
- Features
- Architecture
- Project Structure
- Technology Stack
- How It Works
- Supported Commands
- Data Model
- Concurrency Model
- Thread Safety
- Locking Strategy
- Lazy Expiration
- Persistence
- Persistence Format
- Graceful Shutdown
- Networking
- Protocol
- Building Locally
- Running Locally
- Testing With Netcat
- Testing TTL
- Testing Persistence
- Docker
- Docker Compose
- Persistent Storage
- CI/CD
- Design Decisions
- Trade-offs
- Limitations
- Future Improvements
- Learning Outcomes
- Project Roadmap
- Build and Run Cheat Sheet
- Author
- License
Mini-Redis is a lightweight Redis-inspired in-memory key-value store implemented from scratch in C++17.
The project provides a standalone TCP server that allows multiple clients to connect concurrently and perform basic key-value operations such as storing, retrieving, deleting, and checking keys.
Unlike a simple command-line key-value store, Mini-Redis introduces several systems-programming concepts:
- TCP networking
- Client/server architecture
- Concurrent client handling
- Thread synchronization
- Shared-memory access
- Key expiration
- Disk persistence
- Graceful shutdown
- Docker containerization
- Continuous integration
The goal of the project is not to reproduce the complete Redis implementation.
Instead, Mini-Redis focuses on understanding the fundamental engineering concepts involved in building a small networked in-memory database from first principles.
- In-memory key-value storage
std::unordered_mapbased implementation- String keys and values
- Basic CRUD-style operations
- POSIX TCP sockets
- Server listens on port
6380 - Multiple clients can connect simultaneously
- Line-oriented command interface
- Client/server communication over TCP
- Thread-per-client architecture
std::threadfor concurrent client handlingstd::shared_mutexfor shared store synchronization- Thread-safe access to the underlying data structure
- Per-key TTL
EXPIREcommandTTLcommand- Lazy deletion of expired keys
- Expiration state restored from snapshots
- Disk snapshotting
- Snapshot loaded during startup
- Snapshot written during graceful shutdown
- Expiration metadata persisted
- Expired entries skipped during restoration
- Multi-stage Docker build
- Docker Compose configuration
- Persistent volume mapping
- GitHub Actions CI workflow
Mini-Redis follows a simple layered architecture.
┌──────────────────────┐
│ Client │
│ │
│ netcat / TCP client │
└──────────┬───────────┘
│
│ TCP
▼
┌──────────────────────┐
│ TCP Server │
│ │
│ socket() │
│ bind() │
│ listen() │
│ accept() │
└──────────┬───────────┘
│
│ client socket
▼
┌──────────────────────┐
│ Client Thread │
│ │
│ recv() │
│ parse command │
│ execute command │
│ send() │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Store │
│ │
│ unordered_map │
│ shared_mutex │
│ TTL / expiration │
└──────────┬───────────┘
│
│ graceful shutdown
▼
┌──────────────────────┐
│ Snapshot File │
│ │
│ miniredis_dump.txt │
└──────────────────────┘
The system can be viewed as two primary components:
Networking Layer
│
▼
TCP Server
│
▼
Command Handling
│
▼
Storage Layer
│
▼
Store
The networking layer is responsible for accepting connections and communicating with clients.
The storage layer is responsible for maintaining data, enforcing synchronization, handling expiration, and managing persistence.
MiniRedis/
│
├── main.cpp
├── store.cpp
├── store.h
│
├── Dockerfile
├── docker-compose.yml
├── .gitignore
│
├── .github/
│ └── workflows/
│ └── ci.yml
│
└── data/
└── miniredis_dump.txt
Contains the server-side functionality:
- TCP socket creation
- Address binding
- Listening for connections
- Client acceptance
- Client thread creation
- Command parsing
- Command dispatch
- Response handling
- Server lifecycle management
Defines the storage abstraction.
The Store class owns the in-memory data and exposes operations for:
- setting values
- getting values
- deleting keys
- checking key existence
- configuring expiration
- retrieving TTL
- listing keys
- saving snapshots
- loading snapshots
Contains the implementation of the Store class.
It handles:
unordered_mapoperations- synchronization
- lazy expiration
- TTL calculations
- snapshot serialization
- snapshot restoration
Provides a multi-stage Docker build.
The first stage compiles the application.
The runtime stage contains the resulting executable and required runtime environment.
Provides a convenient containerized deployment configuration with:
- port mapping
- persistent storage
- container lifecycle management
Provides automated CI validation for:
- C++ compilation
- Docker image creation
- Docker image verification
| Technology | Purpose |
|---|---|
| C++17 | Core implementation |
std::unordered_map |
In-memory key-value storage |
| POSIX Sockets | TCP networking |
std::thread |
Concurrent client handling |
std::shared_mutex |
Thread synchronization |
std::chrono |
TTL and expiration management |
| File Streams | Snapshot persistence |
| Linux/POSIX | Runtime environment |
| Docker | Containerization |
| Docker Compose | Container orchestration |
| Bash | Development and operational commands |
| GitHub Actions | Continuous integration |
The server follows the traditional TCP server lifecycle:
┌──────────────┐
│ Start │
└──────┬───────┘
│
▼
Load Snapshot
│
▼
socket()
│
▼
bind()
│
▼
listen()
│
▼
┌──────────────┐
│ accept() │◄─────────┐
└──────┬───────┘ │
│ │
▼ │
Create Thread │
│ │
▼ │
Handle Client │
│ │
└──────────────────┘
At startup, the server attempts to restore previously persisted state.
It then creates a TCP socket, binds it to port 6380, starts listening, and continuously accepts incoming connections.
For every accepted connection:
Client
│
│ connect()
▼
Server
│
│ accept()
▼
Client Socket
│
│ create thread
▼
Client Handler
│
├── recv()
├── parse
├── execute
├── send()
│
└── repeat
The client thread continues processing commands until:
- the client disconnects
- the client sends
QUIT - the client sends
EXIT - an I/O error occurs
Commands follow a simple text-based structure:
COMMAND ARGUMENTS
For example:
SET name Sumit
The server:
- Receives the command.
- Parses the command into tokens.
- Normalizes the command name.
- Validates the number of arguments.
- Calls the appropriate
Storeoperation. - Generates a response.
- Sends the response to the client.
Conceptually:
TCP Input
│
▼
Receive Command
│
▼
Parse
│
▼
Validate
│
▼
Dispatch
│
├── SET
├── GET
├── DEL
├── EXISTS
├── EXPIRE
├── TTL
└── KEYS
│
▼
Store Operation
│
▼
Response
│
▼
TCP Client
Stores a value against a key.
SET <key> <value>
SET name Sumit
Response:
+OK
Retrieves the value associated with a key.
GET <key>
SET name Sumit
GET name
Response:
Sumit
If the key does not exist:
(nil)
Deletes a key from the store.
DEL <key>
SET name Sumit
DEL name
Response:
1
If the key does not exist, the command returns:
0
Checks whether a key currently exists.
EXISTS <key>
SET name Sumit
EXISTS name
Response:
1
For a missing key:
0
Expired keys are treated as non-existent.
Sets a time-to-live for a key.
EXPIRE <key> <seconds>
SET session abc123
EXPIRE session 60
Response:
1
The key will become logically expired after the specified duration.
Returns the remaining lifetime of a key.
TTL <key>
SET session abc123
EXPIRE session 60
TTL session
Example response:
59
The implementation uses the following return values:
-2 → key does not exist
-1 → key exists but has no expiration
>=0 → remaining TTL in seconds
Lists keys currently present in the store.
KEYS
SET name Sumit
SET language C++
KEYS
Possible response:
language
name
Expired entries encountered during the operation are removed lazily.
Closes the current client connection.
QUIT
or:
EXIT
The server closes the connection without shutting down the entire server.
Mini-Redis stores each key using a CacheItem.
Conceptually:
struct CacheItem {
std::string value;
bool has_expiry;
TimePoint expiry_time;
};The primary container is:
std::unordered_map<std::string, CacheItem> data;This allows each key to contain:
Key
│
├── Value
│
├── Has Expiry?
│
└── Expiry Timestamp
For example:
"name"
│
├── value: "Sumit"
└── expiry: none
"session"
│
├── value: "abc123"
└── expiry: timestamp
Mini-Redis uses a thread-per-client concurrency model.
Instead of processing all clients sequentially:
Client A ──┐
Client B ──┼──► Single Server Thread
Client C ──┘
the server creates a separate thread for each client:
Client A ─────► Thread A
Client B ─────► Thread B
Client C ─────► Thread C
Client D ─────► Thread D
All of these threads share the same Store instance.
Therefore, access to the shared store must be synchronized.
The underlying std::unordered_map is shared by multiple client threads.
Concurrent access without synchronization could result in:
- data races
- undefined behavior
- container corruption
- inconsistent reads
- crashes
Mini-Redis protects the shared store using:
std::shared_mutex rw_lock;Operations acquire the appropriate lock before accessing shared state.
Mini-Redis uses std::unique_lock for operations that require exclusive access.
Conceptually:
std::unique_lock<std::shared_mutex> lock(rw_lock);This provides mutual exclusion between concurrent operations that access or modify the store.
Operations such as:
SET
DEL
EXPIRE
naturally require exclusive access.
However, operations such as:
GET
EXISTS
TTL
can also modify the map because of lazy expiration.
For example:
GET expired_key
│
▼
Check expiration
│
├───────────────┐
│ │
Not expired Expired
│ │
▼ ▼
Return value Erase key
│
▼
Return nil
Therefore these operations also require exclusive access in the current design.
The store contains an internal expiration helper:
bool is_expired(const std::string& key);The helper assumes that the caller already holds the required lock.
This design avoids attempting to acquire the same shared_mutex again inside an operation that already owns it.
For example:
GET
│
├── acquire unique_lock
│
├── is_expired()
│
├── possibly erase expired key
│
└── release lock
Instead of:
GET
│
├── acquire lock
│
└── is_expired()
│
└── acquire same lock again ← problematic
This makes the locking responsibility explicit and avoids nested locking of the same synchronization primitive.
Mini-Redis uses lazy expiration.
When a key receives a TTL:
SET session abc123
EXPIRE session 10
the store records the expiration timestamp.
It does not continuously scan the entire store looking for expired keys.
Instead, expiration is checked when a relevant operation accesses the key.
For example:
Key Access
│
▼
Check expiration
│
┌───────┴────────┐
│ │
Valid Expired
│ │
▼ ▼
Return value Erase key
A background expiration mechanism would require an additional worker thread or periodic scanning mechanism.
Lazy expiration keeps the implementation smaller and easier to reason about.
- Simple implementation
- No dedicated expiration thread
- No periodic full-store scan
- Fewer synchronization requirements
- Expiration work occurs only when needed
An expired key that is never accessed may remain physically present in the underlying map until an operation encounters it.
The key is nevertheless treated as expired by operations that perform expiration checks.
Mini-Redis uses snapshot-based persistence.
The in-memory state can be serialized to disk and restored when the server starts again.
The persistence lifecycle is:
Server Startup
│
▼
Load Snapshot
│
▼
Restore Store
│
▼
Accept Clients
│
▼
Normal Runtime
│
▼
Graceful Termination
│
▼
Save Snapshot
│
▼
Server Stops
The snapshot contains the current state of the in-memory store.
For each entry, the snapshot records:
KEY VALUE HAS_EXPIRY EXPIRY_TIMESTAMP
Example:
username Sumit 0 0
language cpp 0 0
session abc123 1 1790000000
Where:
HAS_EXPIRY = 0
means the key has no expiration.
And:
HAS_EXPIRY = 1
means an expiration timestamp is stored.
When Mini-Redis starts, it attempts to load the snapshot.
The server reconstructs the in-memory state from the persisted representation.
For keys that have an expiration timestamp, the server checks whether the key has already expired.
Expired entries are not restored.
Conceptually:
Snapshot
│
▼
Read Entry
│
▼
Has Expiry?
│
├── No ──────► Restore
│
└── Yes
│
▼
Already Expired?
│
├── No ──► Restore
│
└── Yes ─► Skip
Mini-Redis supports graceful server termination through termination signals such as:
SIGINT
SIGTERM
A normal graceful shutdown performs the persistence step before the process exits.
For example:
Ctrl+C
│
▼
Graceful Shutdown
│
▼
Save Store Snapshot
│
▼
Exit
This allows the current in-memory state to survive a normal server shutdown.
Important: Snapshot persistence protects state across graceful shutdown. It is not crash-consistent write-ahead logging. A sudden process crash, machine failure, or termination before the snapshot is written can still result in lost changes.
Mini-Redis uses the POSIX socket API to implement a TCP server.
The basic server lifecycle is:
socket()
│
▼
bind()
│
▼
listen()
│
▼
accept()
│
▼
recv()
│
▼
Process Command
│
▼
send()
The server listens on:
Host: 0.0.0.0
Port: 6380
Protocol: TCP
A TCP client connects to the server:
Client
│
│ TCP Connection
▼
Mini-Redis
Once connected, the client can send multiple commands.
The server processes commands and sends responses over the same TCP connection.
This makes the server stateful at the connection level while keeping the actual key-value state shared across all clients.
Mini-Redis currently uses a simple line-oriented text protocol rather than Redis's RESP protocol.
Commands are represented as:
COMMAND ARGUMENTS\n
Examples:
SET name Sumit
GET name
DEL name
EXISTS name
EXPIRE name 60
TTL name
KEYS
The protocol was intentionally kept simple to focus on the underlying concepts of:
- TCP networking
- server lifecycle
- concurrent connections
- synchronization
- storage
rather than implementing the complete Redis wire protocol.
A client can communicate with Mini-Redis using:
SET name Sumit
The server responds:
+OK
Then:
GET name
returns:
Sumit
A complete interaction may look like:
Client Server
│ │
│──── SET name Sumit ─────────►│
│ │
│◄────────── +OK ──────────────│
│ │
│──── GET name ───────────────►│
│ │
│◄────────── Sumit ────────────│
│ │
│──── EXISTS name ────────────►│
│ │
│◄──────────── 1 ──────────────│
A POSIX-compatible environment such as:
- Linux
- WSL
- another Unix-like development environment
is recommended.
Required tools:
- C++17-compatible compiler
- POSIX socket support
- pthread support
Recommended tools:
netcat- Git
- Docker
- Docker Compose
Verify your compiler:
g++ --versionMini-Redis requires C++17.
Compile the project with:
g++ -std=c++17 main.cpp store.cpp -pthread -o miniredisFor a stricter development build:
g++ -std=c++17 \
-Wall \
-Wextra \
-Wpedantic \
-Wconversion \
-Wsign-conversion \
main.cpp store.cpp \
-pthread \
-o miniredisStart the server:
./miniredisThe server listens on:
0.0.0.0:6380
Open another terminal and connect using:
nc localhost 6380You can now issue commands directly.
Example:
SET name Sumit
+OK
GET name
Sumit
EXISTS name
1
DEL name
1
GET name
(nil)
Open multiple terminals.
nc localhost 6380nc localhost 6380nc localhost 6380Each connection is handled by its own server thread.
Because all clients operate on the same Store instance, changes made from one client are visible to the others.
Example:
SET shared hello
GET shared
Result:
hello
Connect to the server:
nc localhost 6380Create a key:
SET temporary hello
Set an expiration:
EXPIRE temporary 10
Check the TTL:
TTL temporary
The result should be approximately:
9
After the TTL expires:
GET temporary
returns:
(nil)
The expired key is removed lazily.
Start Mini-Redis:
./miniredisStore some values:
SET username Sumit
SET language cpp
SET project MiniRedis
Gracefully terminate the server:
Ctrl+C
The server writes its current state to the snapshot file.
Start Mini-Redis again:
./miniredisThe state is loaded from the snapshot.
Verify:
GET username
Expected:
Sumit
Then:
GET language
Expected:
cpp
And:
GET project
Expected:
MiniRedis
Mini-Redis includes a multi-stage Dockerfile.
The build process separates:
Build Environment
│
│ compile
▼
C++ Executable
│
│ copy
▼
Runtime Image
This avoids requiring the complete compiler toolchain in the final runtime image.
Run:
docker build -t miniredis .Verify:
docker image lsRun the container:
docker run --rm -p 6380:6380 miniredisThen connect:
nc localhost 6380The application is now running inside the container while the TCP port is exposed to the host.
Mini-Redis also provides a Docker Compose configuration.
Start the application:
docker compose up --buildRun in detached mode:
docker compose up --build -dCheck the service:
docker compose psView logs:
docker compose logs -fStop the service:
docker compose downThe Docker Compose configuration maps the host data directory to the container's data directory.
Conceptually:
Host
│
└── ./data
│
│ volume mapping
▼
Container
│
└── /data
This means persistence is not tied exclusively to the lifetime of a container.
The snapshot can remain available on the host and be reused when the service is recreated.
Source Code
│
▼
Docker Build
│
▼
Compiled Binary
│
▼
Runtime Container
│
├──────── TCP :6380
│
└──────── /data
│
▼
Persistent
Storage
Mini-Redis uses GitHub Actions for automated build validation.
The CI workflow runs when changes are pushed to the repository's main branch or when pull requests target the main branch.
The workflow performs:
Checkout Repository
│
▼
Compile C++ Application
│
▼
Build Docker Image
│
▼
Verify Docker Image
This provides an automated check that the project remains buildable and that the container image can be created successfully.
The workflow uses:
GitHub Actions
│
├── Checkout
│
├── C++ Build
│
├── Docker Build
│
└── Docker Image Verification
This prevents basic compilation and containerization failures from going unnoticed.
Mini-Redis is primarily a key-based lookup system.
std::unordered_map provides average constant-time complexity for:
- lookup
- insertion
- deletion
This makes it a natural data structure for a basic in-memory key-value store.
Conceptually:
Key
│
▼
Hash Function
│
▼
Bucket
│
▼
Value
C++ was selected because the project is intended to explore systems-level concepts.
It provides direct access to:
- threads
- synchronization primitives
- file I/O
- socket APIs
- memory-oriented data structures
- low-level operating-system interfaces
This makes the language particularly suitable for understanding the mechanics behind a networked in-memory server.
POSIX sockets provide a direct interface to TCP networking.
Using them exposes the core lifecycle of a network server:
socket
bind
listen
accept
recv
send
Instead of hiding these details behind a high-level networking framework, Mini-Redis implements the basic server lifecycle directly.
The thread-per-client model was selected because it is straightforward to understand and implement.
Each connection gets its own execution context:
Connection 1 → Thread 1
Connection 2 → Thread 2
Connection 3 → Thread 3
- Simple mental model
- Easy client isolation
- Straightforward blocking I/O
- Natural mapping between client and thread
- One thread per connection
- Thread creation overhead
- Large numbers of clients can create resource pressure
- Less scalable than event-driven I/O for very high connection counts
The architecture is appropriate for the scope of Mini-Redis.
A std::shared_mutex allows a design to distinguish between shared and exclusive access.
Conceptually:
Shared Lock
│
├── Reader A
├── Reader B
└── Reader C
Exclusive Lock
│
└── Writer
However, because Mini-Redis performs lazy deletion during operations such as GET, EXISTS, and TTL, those operations may modify the map.
Therefore the current implementation uses exclusive locking where lazy expiration can occur.
This prioritizes correctness and clear synchronization semantics over attempting to maximize read parallelism prematurely.
A background expiration worker could periodically scan for expired keys.
However, that introduces another concurrent component:
Client Threads
│
├──────────────┐
│ │
▼ ▼
Store Expiration
Worker
That would require additional synchronization and lifecycle management.
Lazy expiration avoids this additional complexity.
For the scope of Mini-Redis, the trade-off is appropriate.
Snapshot persistence was selected instead of AOF because it provides a simple mechanism for restoring the in-memory state without requiring every write command to be logged.
Current Memory
│
▼
Snapshot File
SET ...
SET ...
DEL ...
SET ...
...
Mini-Redis focuses on snapshot persistence to keep the system compact and understandable.
Redis uses the Redis Serialization Protocol (RESP).
Mini-Redis currently uses a simpler line-oriented protocol.
Implementing RESP would provide greater compatibility with Redis clients, but would also add protocol parsing and serialization complexity.
The current project prioritizes understanding:
- TCP
- concurrency
- synchronization
- storage
- persistence
over Redis protocol compatibility.
Every architectural decision in Mini-Redis involves a trade-off.
| Decision | Advantage | Trade-off |
|---|---|---|
unordered_map |
Fast average key lookup | No ordering |
| Thread-per-client | Simple concurrency model | Thread overhead |
shared_mutex |
Supports shared/exclusive semantics | More synchronization complexity |
| Lazy expiration | Simple, no background worker | Expired entries may remain until encountered |
| Snapshot persistence | Simple restoration | Recent changes can be lost after unexpected failure |
| Text protocol | Easy to debug | Not Redis-compatible RESP |
| Docker | Reproducible environment | Adds containerization overhead |
The server validates command names and argument counts before executing operations.
Malformed commands should result in an error response rather than terminating the server.
Examples:
UNKNOWN
SET
GET
EXPIRE key
may result in an error response similar to:
-ERR unknown command or wrong number of arguments
The goal is to keep malformed client input isolated to the individual request.
Mini-Redis is intentionally not a production Redis replacement.
Current limitations include:
- Simple text-based protocol
- No RESP support
- No AOF
- No replication
- No clustering
- No transactions
- No authentication
- No advanced Redis data types
- No LRU eviction
- No memory limit
- No distributed operation
- Thread-per-client architecture
- Snapshot-based rather than crash-consistent persistence
- Simple command parsing
- String-only values
These limitations are intentional and keep the project focused on the core systems concepts.
The architecture provides several possible directions for future development.
- RESP support
- Binary-safe values
- More robust TCP stream framing
- Better command parsing
- More Redis-compatible responses
- Fixed-size thread pool
- Work queue
- Asynchronous I/O
- Event-driven architecture
- Linux
epoll
- Append-Only File (AOF)
- Periodic snapshots
- Atomic snapshot replacement
- Crash recovery
- Snapshot versioning
- Corruption detection
- Configurable memory limits
- LRU eviction
- Memory usage statistics
Potential additional data structures:
Lists
Sets
Hashes
Sorted Sets
Potential additions:
- Request counters
- Connected client count
- Operation latency
- Throughput metrics
- Server statistics
- Logging
Mini-Redis was developed incrementally around several systems concepts.
- In-memory hash map
- Basic CRUD operations
- Command-line interaction
- POSIX socket server
- TCP client connections
- Command/response protocol
- SET
- GET
- DEL
- EXISTS
- EXPIRE
- TTL
- KEYS
- Error handling
- Lazy expiration
- Multiple simultaneous clients
std::thread- Shared store
- Synchronization
std::shared_mutex
- Snapshot serialization
- Startup restoration
- Expiration restoration
- Graceful shutdown persistence
- Docker
- Docker Compose
- Persistent volumes
- GitHub Actions CI
The project deliberately stops short of implementing advanced Redis functionality so that each implemented component remains understandable and discussable.
g++ -std=c++17 main.cpp store.cpp -pthread -o miniredis./miniredisnc localhost 6380docker build -t miniredis .docker run --rm -p 6380:6380 miniredisStart:
docker compose up --buildDetached:
docker compose up --build -dStatus:
docker compose psLogs:
docker compose logs -fStop:
docker compose downStart the server:
./miniredisConnect:
nc localhost 6380Store data:
SET name Sumit
+OK
SET language C++
+OK
Retrieve data:
GET name
Sumit
GET language
C++
Check existence:
EXISTS name
1
Set an expiration:
EXPIRE name 30
1
Check TTL:
TTL name
29
List keys:
KEYS
language
name
Delete a key:
DEL name
1
Verify deletion:
GET name
(nil)
Close the client:
QUIT
Mini-Redis provides practical exposure to several core computer-science and systems concepts.
- Hash tables
- Key-value storage
- Hash-based lookup
- Average O(1) lookup/insertion/deletion
- Processes
- Threads
- Synchronization
- Signals
- File I/O
- Shared state
- TCP
- Sockets
- Client/server architecture
- Blocking I/O
- Connection lifecycle
- Network protocols
- Race conditions
- Mutual exclusion
- Shared resources
- Thread synchronization
- Read/write synchronization
- Thread-per-client architecture
- Serialization
- Deserialization
- Snapshots
- State restoration
- Expiration timestamps
- Docker
- Multi-stage builds
- Docker Compose
- Persistent volumes
- CI workflows
For the underlying hash map, the expected average complexity is:
| Operation | Average Complexity |
|---|---|
SET |
O(1) |
GET |
O(1) |
DEL |
O(1) |
EXISTS |
O(1) |
EXPIRE |
O(1) |
TTL |
O(1) |
KEYS |
O(n) |
KEYS requires traversing the store and is therefore linear in the number of stored keys.
Expiration checks performed during key-specific operations are constant-time on average because they first perform a hash-map lookup.
Mini-Redis is a learning and portfolio project and should not be exposed directly to an untrusted public network.
The current implementation does not provide:
- Authentication
- Authorization
- TLS
- Encryption
- Rate limiting
- Resource quotas
- Production-grade input hardening
For local development and controlled environments, the server is sufficient for demonstrating the intended systems concepts.
The purpose of Mini-Redis is to bridge the gap between theoretical computer-science knowledge and practical systems programming.
Rather than interacting with a database through a high-level API, the project implements the major building blocks directly:
┌──────────────────┐
│ Data Structure │
│ unordered_map │
└────────┬─────────┘
│
┌────────▼─────────┐
│ Synchronization│
│ shared_mutex │
└────────┬─────────┘
│
┌────────▼─────────┐
│ Networking │
│ TCP Sockets │
└────────┬─────────┘
│
┌────────▼─────────┐
│ Persistence │
│ Snapshots │
└────────┬─────────┘
│
┌────────▼─────────┐
│ Deployment │
│ Docker + CI │
└──────────────────┘
The resulting system is small enough to understand end-to-end while still exposing important concepts used in real server-side systems.
Mini-Redis can be used to discuss several systems-programming topics in technical interviews.
- Why TCP instead of UDP?
- What does
socket()return? - Why are
bind(),listen(), andaccept()separate operations? - What is the difference between the listening socket and a client socket?
- How does a TCP connection work?
- Why use one thread per client?
- What happens when multiple clients modify the same key?
- Why is synchronization necessary?
- Why use
shared_mutex? - What is a race condition?
- What happens if the map is accessed concurrently without synchronization?
- Why lazy expiration?
- Why can
GETrequire an exclusive lock? - What happens to an expired key that is never accessed?
- How would a background expiration worker change the design?
- Why snapshotting?
- What happens if the server crashes before a snapshot?
- How does startup restoration work?
- How could AOF improve durability?
- How could snapshots be made atomic?
- What are the limitations of thread-per-client?
- How would a thread pool improve the design?
- How would
epollchange the architecture? - How would the system behave with thousands of simultaneous clients?
The main engineering concepts demonstrated by Mini-Redis are:
✓ C++17 systems programming
✓ POSIX TCP networking
✓ Concurrent client handling
✓ Thread synchronization
✓ Shared state management
✓ TTL-based expiration
✓ Lazy deletion
✓ Snapshot persistence
✓ Graceful shutdown
✓ Docker containerization
✓ Docker Compose
✓ GitHub Actions CI
GitHub:
https://github.com/mesumittiwari/MiniRedis
B.Tech — Mathematics and Computing
Madhav Institute of Technology and Science, Gwalior
- GitHub: https://github.com/mesumittiwari
- LinkedIn: https://linkedin.com/in/mesumittiwari
- LeetCode: https://leetcode.com/mesumittiwari
This project is intended primarily as a learning and portfolio project.
If you choose to add an open-source license, place the corresponding license text in a LICENSE file in the repository.
Built from scratch with C++17 to understand networking, concurrency, persistence, and systems programming.