A from-scratch Redis-protocol-compatible key/value server in Java 21 + Netty. Speaks RESP2, supports GET/SET/DEL/EXISTS/EXPIRE/TTL/KEYS/INCR/FLUSHALL/PING/ECHO. Eviction policies (LRU/LFU/FIFO), background expiration sweeper, ~500 LOC.
$ mvn -q -DskipTests package
$ java -jar target/redis-clone-1.0.0.jar 6379
# Talk to it with the real redis-cli:
$ redis-cli -p 6379
127.0.0.1:6379> PING
PONG
127.0.0.1:6379> SET mykey "hello" EX 60
OK
127.0.0.1:6379> GET mykey
"hello"
127.0.0.1:6379> TTL mykey
(integer) 59
127.0.0.1:6379> INCR counter
(integer) 1
127.0.0.1:6379> INCR counter
(integer) 2
127.0.0.1:6379> KEYS *
1) "mykey"
2) "counter"
127.0.0.1:6379> DEL mykey
(integer) 1
It is a drop-in protocol-compatible replacement for the subset of Redis commands listed above. Real redis-cli connects without modification.
| Capability | Where |
|---|---|
| RESP2 codec from scratch | ProtocolParser — array-of-bulk-strings, bulk, simple, integer, error; inline-command fallback for redis-cli handshake quirks |
| Netty pipeline | RedisServer boots an NioEventLoopGroup, one ClientHandler instance per connection (Netty handles framing + concurrency) |
| Thread-safe store | DataStore uses ConcurrentHashMap for reads, synchronized for the set/delete/evict triad that has to atomically maintain memory accounting |
| Active + passive expiration | RedisValue.isExpired() checks lazily on every access; background ScheduledExecutorService sweeps every second to release memory from cold expired keys |
| Pluggable eviction policies | LRU (default), LFU, FIFO — switch by comparator. Evicts 25% of keys when memory exceeds limit |
| Bounded memory | maxMemoryBytes arg + estimateSize heuristic per value type; eviction triggers before OOM |
git clone https://github.com/mohidev-tech/redis-clone
cd redis-clone
mvn -DskipTests package
java -jar target/redis-clone-1.0.0.jar 6379
# In another terminal — drives the server with the actual redis-cli:
redis-cli -p 6379 PINGdocker build -t redis-clone .
docker run -p 6379:6379 redis-clonejava -jar redis-clone-1.0.0.jar [PORT] [MAX_MEMORY_BYTES]
↑ default 6379
↑ default 1_000_000_000 (1 GB)
Not to replace Redis. To understand Redis. The RESP protocol is small enough to implement in an afternoon, and doing so teaches:
- How a binary protocol packs into TCP frames (length-prefixed bulk strings, CRLF terminators).
- Why Redis chose single-threaded data-plane + multi-event-loop I/O.
- How active+passive expiration trades memory for CPU.
- Where the eviction policy decision lives (it's not just "LRU vs LFU" — it's "what's your access-pattern fingerprint").
| Command | Status |
|---|---|
GET key |
✅ |
SET key value [EX seconds] |
✅ |
DEL key [key ...] |
✅ |
EXISTS key [key ...] |
✅ |
EXPIRE key seconds |
✅ |
TTL key |
✅ (-1 = no expiry, -2 = missing) |
KEYS pattern |
✅ (glob: *, ?) |
INCR key |
✅ |
FLUSHALL |
✅ |
PING |
✅ |
ECHO message |
✅ |
COMMAND |
redis-cli handshake) |
| Lists, sets, hashes, sorted sets, pub/sub, streams, persistence (RDB/AOF) | ❌ Out of scope |
| Choice | Why |
|---|---|
Netty SimpleChannelInboundHandler, not raw NIO |
Netty does framing, backpressure, and the event loop. Reimplementing those is its own portfolio project; for this one we focus on RESP + the store |
ConcurrentHashMap for the hot read path |
Get/exists/ttl are lock-free. The synchronized set/delete/evict only contends when memory accounting changes |
| Estimate size by type, not by serializing | Strings: 2 * length. Lists/sets/maps: count × heuristic-per-element. Wrong in absolute terms, right enough relative to itself — eviction triggers proportionally as data grows |
| Inline command fallback in the parser | redis-cli sends inline PING\r\n during connect; without this fallback the handshake misses and the prompt shows a phantom error |
- No persistence. Data lives in process memory; restart = empty. RDB snapshots + AOF append-only file are real Redis features and out of scope for a learning exercise.
- Single-node only. No replication, no cluster, no consistent hashing. Add MASTER/REPLICATION + sentinel later.
- No auth. Anyone who can connect can read+write. Pair with a network policy.
- No pipelining optimization. Pipelining works (Netty handles it) but we don't batch-write the response — small perf loss vs real Redis.
PRs welcome. See CONTRIBUTING.md. Security issues: SECURITY.md.