A lightweight, Redis-inspired in-memory cache database built from scratch with Node.js, implementing the RESP protocol, TCP server communication, data persistence, and production-ready monitoring.
This project is a functional in-memory cache database that mimics Redis behavior, built to understand distributed systems, protocol design, and production deployment considerations. It implements:
- ✅ RESP Protocol - Redis Serialization Protocol for wire-compatible communication
- ✅ TCP Server - Native Node.js TCP server for handling client connections
- ✅ 13+ Commands - Redis-compatible command set (PING, SET, GET, EXPIRE, etc.)
- ✅ Data Persistence - Snapshot-based persistence with auto-save
- ✅ Prometheus Metrics - Production monitoring and observability
- ✅ Docker & Kubernetes - Cloud-native deployment ready
- ✅ Interactive CLI - User-friendly REPL client
┌─────────────────────────────────────────────────────────────┐
│ Client Layer │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ CLI REPL │ │ netcat │ │ Custom Clients │ │
│ └──────┬──────┘ └──────┬───────┘ └────────┬─────────┘ │
└─────────┼─────────────────┼──────────────────┼──────────────┘
│ │ │
└─────────────────┴──────────────────┘
│ RESP Protocol over TCP
▼
┌─────────────────────────────────────┐
│ TCP Server (port 6379) │
│ ┌──────────────────────────────┐ │
│ │ Connection Manager │ │
│ │ - Buffer Management │ │
│ │ - RESP Parser │ │
│ │ - Command Router │ │
│ └──────────────────────────────┘ │
└─────────────┬───────────────────────┘
│
┌─────────────▼───────────────────────┐
│ Command Handlers Layer │
│ ┌────────┬────────┬─────────────┐ │
│ │ Server │ Key │ String │ │
│ │ Ops │ Ops │ Ops │ │
│ └────────┴────────┴─────────────┘ │
└─────────────┬───────────────────────┘
│
┌─────────────▼───────────────────────┐
│ Storage Layer │
│ ┌────────────┐ ┌──────────────┐ │
│ │ Store │ │ TTL Map │ │
│ │ (Object) │ │ (Timestamps)│ │
│ └─────┬──────┘ └──────┬───────┘ │
└────────┼─────────────────┼───────────┘
│ │
┌────────▼─────────────────▼───────────┐
│ Persistence (db.json) │
│ Auto-save every 5 minutes │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ Prometheus Metrics (port 9000) │
│ - Active Connections │
│ - Commands Processed │
│ - Runtime Metrics │
└──────────────────────────────────────┘
- Node.js: Event-driven, non-blocking I/O for concurrent connections
- TCP Protocol: Native
netmodule for low-latency communication - RESP: Binary protocol parsing with buffer management
- Prometheus: Observability and metrics collection
- Docker/K8s: Containerization and orchestration
- Node.js 18+
- npm or yarn
- (Optional) Docker & Docker Compose
-
Clone the repository:
git clone https://github.com/saitadikonda99/Own-Cache-Database.git cd Own-Cache-Database -
Install dependencies:
npm install
-
Start the server:
node src/server.js
Server starts on
0.0.0.0:6379(Redis default port) -
Connect with the CLI client:
npm start # or node src/client/client.js
Configure server settings via environment variables:
export HOST=127.0.0.1
export PORT=6379
node src/server.js| Command | Description | Example | Response |
|---|---|---|---|
PING |
Health check | PING |
PONG |
| Command | Description | Example | Response |
|---|---|---|---|
DEL key |
Delete a key | DEL name |
(integer) 1 |
EXISTS key |
Check key existence | EXISTS name |
1 or 0 |
KEYS pattern |
Find keys by pattern | KEYS user:* |
List of matching keys |
TYPE key |
Get value type | TYPE counter |
string or number |
PERSIST key |
Remove expiration | PERSIST session |
1 or 0 |
| Command | Description | Example | Response |
|---|---|---|---|
EXPIRE key sec |
Set TTL in seconds | EXPIRE session 3600 |
1 or 0 |
TTL key |
Get remaining TTL | TTL session |
3599 or -1 (no expiry) or -2 (not exists) |
| Command | Description | Example | Response |
|---|---|---|---|
SET key value [EX seconds] |
Store value (with optional TTL) | SET name "Alice" EX 60 |
OK |
GET key |
Retrieve value | GET name |
"Alice" or (nil) |
APPEND key value |
Append to string | APPEND greeting "World" |
10 (new length) |
INCR key |
Increment by 1 | INCR counter |
1, 2, 3... |
DECR key |
Decrement by 1 | DECR counter |
0, -1, -2... |
Own-Cache-Database:6379> PING
PONG
Own-Cache-Database:6379> SET user:1:name "John Doe" EX 300
OK
Own-Cache-Database:6379> GET user:1:name
"John Doe"
Own-Cache-Database:6379> TTL user:1:name
(integer) 295
Own-Cache-Database:6379> INCR user:1:visits
(integer) 1
Own-Cache-Database:6379> KEYS user:*
1) "user:1:name"
2) "user:1:visits"
Own-Cache-Database:6379> exit# Build image
docker build -t own-cache-db .
# Run server
docker run -p 6379:6379 -p 9000:9000 own-cache-db# Start both server and client
docker compose up -d
# Attach to interactive client
docker attach cache-client
# View logs
docker compose logs -f cache-server
# Stop services
docker compose downThe docker-compose.yml includes:
- cache-server: TCP server on port 6379
- cache-client: Interactive CLI client (attach to use)
Full production-ready Kubernetes setup included:
# Deploy server
kubectl apply -f k8s/cache-server/
# Deploy client
kubectl apply -f k8s/cache-client/
# Deploy monitoring stack
kubectl apply -f k8s/prometheus/
kubectl apply -f k8s/grafana/
# Check status
kubectl get pods
kubectl get services
# Access server
kubectl port-forward service/cache-server 6379:6379
# View metrics
kubectl port-forward service/prometheus 9090:9090- Cache Server: Deployment + ClusterIP Service
- Cache Client: Deployment for testing
- Prometheus: Metrics collection with RBAC
- Grafana: Visualization dashboards
Exposed on http://localhost:9000/metrics:
tcp_active_connections(Gauge) - Current connected clientstcp_commands_processed_total(Counter) - Total commands executed- Node.js runtime metrics (memory, CPU, event loop)
# Request rate
rate(tcp_commands_processed_total[1m])
# Active connections
tcp_active_connections
# Memory usage
process_resident_memory_bytes
Send raw RESP protocol commands:
# PING command
echo -e "*1\r\n\$4\r\nPING\r\n" | nc localhost 6379
# SET command
echo -e "*3\r\n\$3\r\nSET\r\n\$4\r\nname\r\n\$5\r\nAlice\r\n" | nc localhost 6379
# GET command
echo -e "*2\r\n\$3\r\nGET\r\n\$4\r\nname\r\n" | nc localhost 6379npm startOwn-Cache-Database/
├── src/
│ ├── server.js # TCP server & main entry point
│ ├── client/
│ │ └── client.js # Interactive CLI client
│ ├── command/
│ │ └── commandHandlers.js # Command implementations
│ └── lib/
│ ├── parser/
│ │ ├── respParser.js # RESP protocol encoder/decoder
│ │ └── commandParser.js # User input parser
│ ├── storage/
│ │ └── store.js # In-memory storage
│ └── display/
│ └── responseFormatter.js # Response formatting
├── k8s/ # Kubernetes manifests
│ ├── cache-server/ # Server deployment
│ ├── cache-client/ # Client deployment
│ ├── prometheus/ # Metrics collection
│ └── grafana/ # Visualization
├── docker-compose.yml # Multi-container setup
├── Dockerfile # Container image
├── db.json # Persistence file (auto-generated)
├── package.json # Dependencies
└── README.md # Project documentation
The Redis Serialization Protocol (RESP) is the backbone of client-server communication:
Converts JavaScript data to RESP wire format:
- Strings → Bulk String:
$5\r\nhello\r\n - Numbers → Integer:
:42\r\n - Arrays → Array:
*2\r\n$4\r\nPING\r\n - Null → Null Bulk String:
$-1\r\n - Errors → Error:
-ERR Unknown command\r\n
Parses binary buffers with state management:
// Handles partial data streams
let buffer = Buffer.alloc(0);
socket.on('data', (data) => {
buffer = Buffer.concat([buffer, data]);
const [command, newOffset] = parseRESP(buffer);
if (command === null) return; // Incomplete data
buffer = buffer.subarray(newOffset);
handleCommand(socket, command);
});Key Challenge: Commands may arrive in chunks (e.g., network fragmentation), requiring offset tracking and buffer management.
store = {
"user:1:name": "Alice",
"user:1:visits": 42,
"session:abc123": "..."
}
ttl = {
"session:abc123": 1699999999999 // Unix timestamp (ms)
}- Lazy Expiration: Checked on access (GET, EXISTS, etc.)
- Precision: Millisecond-level timestamps
- Trade-off: Expired keys remain in memory until accessed
- Future Enhancement: Background cleanup task
// Auto-save every 5 minutes
setInterval(saveToDisk, 300000);
// Snapshot format (db.json)
{
"store": {
"key1": "value1",
"key2": "value2"
}
}Limitations:
- Potential data loss window (0-5 minutes)
- TTL not persisted (expires on restart)
- No WAL (Write-Ahead Logging)
Event-Driven Architecture via Node.js:
┌──────────────┐
│ Client 1 │─────┐
└──────────────┘ │
┌──────────────┐ ├──► ┌─────────────────┐
│ Client 2 │─────┤ │ Event Loop │
└──────────────┘ │ │ (Single Thread)│
┌──────────────┐ ├──► └─────────────────┘
│ Client 3 │─────┘ │
└──────────────┘ ▼
┌─────────────────┐
│ Store (Shared) │
└─────────────────┘
Benefits:
- ✅ Non-blocking I/O
- ✅ Low memory overhead
- ✅ Natural fit for I/O-bound workload
Limitations:
- ❌ CPU-bound operations block all clients
- ❌ No multi-core utilization (without clustering)
- Protocol Errors: Invalid RESP format →
-ERR Invalid command\r\n - Command Errors: Wrong arguments →
-ERR wrong number of arguments\r\n - Type Errors: Non-integer INCR →
-ERR value is not an integer\r\n - Socket Errors: Connection drops → Clean up metrics, log event
✅ Pros:
- Event-driven model perfect for I/O-bound cache operations
- Native TCP support with
netmodule - Fast prototyping with JavaScript
❌ Cons:
- Single-threaded limits CPU utilization
- No native support for data structures (lists, sets)
✅ Pros:
- Industry-standard (Redis compatibility)
- Simple to parse
- Human-readable for debugging
❌ Cons:
- More verbose than binary protocols (e.g., MessagePack)
- No built-in compression
✅ Pros:
- Simple implementation
- Low I/O overhead
- Predictable disk usage
❌ Cons:
- Data loss window on crash
- TTL not preserved across restarts
- Large datasets = slow startup
Alternative Considered: Append-Only File (AOF) like Redis
- Would provide better durability
- Higher I/O overhead
- More complex implementation
✅ Pros:
- O(1) lookups
- Low latency
- Simple code
❌ Cons:
- Limited by RAM
- Data lost on crash (mitigated by snapshots)
- No eviction policy (LRU, LFU)
-
Protocol Design
- Binary data parsing with buffers
- Handling streaming data and partial messages
- Importance of explicit length prefixes
-
Distributed Systems Concepts
- Cache invalidation strategies (TTL)
- Trade-offs: consistency vs. performance
- Persistence vs. durability
-
Network Programming
- TCP socket management
- Connection lifecycle (connect, data, error, close)
- Backpressure and flow control
-
Production Considerations
- Observability (metrics, logging)
- Containerization best practices
- Health checks and graceful shutdown
- Caching isn't just "storing data" - it's about expiration, eviction, and consistency
- Protocol choice matters - RESP's simplicity enables wide adoption
- Monitoring is critical - Can't optimize what you don't measure
- Persistence is hard - Balancing durability, performance, and complexity
- Active key expiration (background cleanup)
- Persistent TTL storage
- Append-Only File (AOF) persistence
- Eviction policies (LRU, LFU)
- Data structures (Lists, Sets, Sorted Sets, Hashes)
- Pub/Sub messaging
- Transactions (MULTI/EXEC)
- Pipelining support
- Master-slave replication
- Clustering with consistent hashing
- Authentication (AUTH command)
- SSL/TLS support
- Connection pooling
- Rate limiting
- Batch operations
- Compression (LZ4/Snappy)
"I built a Redis-inspired cache system from scratch to deeply understand distributed systems. It implements the RESP protocol, supports 13+ commands, includes TTL-based expiration, data persistence, and is production-ready with Docker, Kubernetes, and Prometheus monitoring."
- Protocol Implementation: Binary RESP parser handling streaming data
- Production-Ready: Metrics, containerization, K8s manifests
- Full-Stack: TCP server, CLI client, persistence, monitoring
- Standards-Compliant: Redis protocol compatibility
Challenge 1: Partial Message Handling
// Commands can arrive in multiple chunks
// Solution: Buffer accumulation + offset tracking
let buffer = Buffer.alloc(0);
socket.on('data', (data) => {
buffer = Buffer.concat([buffer, data]);
// Parse with offset, keep unparsed data
});Challenge 2: TTL Precision
// Problem: setTimeout unreliable for many keys
// Solution: Lazy expiration with timestamp comparison
if (key in ttl && ttl[key] < Date.now()) {
delete store[key];
delete ttl[key];
}Challenge 3: Type Coercion
// INCR must handle string -> number conversion
const currentValue = parseInt(store[key]);
if (isNaN(currentValue)) {
return error('value is not an integer');
}-
"Why not just use Redis?" → Learning project to understand internals; not for production replacement
-
"How does it handle concurrent connections?" → Node.js event loop; non-blocking I/O; shared memory state
-
"What about data durability?" → Snapshot every 5 min; trade-off for simplicity; could add AOF
-
"How would you scale this?" → Master-slave replication, consistent hashing, sharding by key prefix
-
"Biggest challenge?" → RESP parsing with partial buffers; required careful offset management
- GET/SET/DEL: O(1)
- **KEYS ***: O(n) - scans all keys
- EXPIRE/TTL: O(1)
- Single-threaded: No parallel command execution
- Memory-bound: Dataset limited by RAM
- No eviction: Memory grows unbounded
# Using redis-benchmark (if RESP-compatible)
redis-benchmark -h localhost -p 6379 -t set,get -n 100000
# Custom benchmark script
for i in {1..10000}; do
echo "SET key$i value$i" | nc localhost 6379
doneContributions are welcome! Here's how you can help:
- Additional Redis commands (LPUSH, SADD, HSET, etc.)
- Improved persistence (AOF, RDB snapshots)
- Benchmark suite and performance tests
- Documentation improvements
- Bug fixes and error handling
# Fork & clone
git clone https://github.com/YOUR_USERNAME/Own-Cache-Database.git
# Install dependencies
npm install
# Run in dev mode with auto-reload
npm run dev
# Test changes
node src/client/client.jsThis project is licensed under the ISC License - see the LICENSE file for details.
- Redis: Inspiration for protocol and command design
- Node.js: Powerful event-driven runtime
- RESP Protocol: Simple yet effective wire protocol
- Prometheus: Industry-standard monitoring
- Redis Protocol Specification
- Node.js Net Module
- Prometheus Best Practices
- Docker Documentation
- Kubernetes Basics