A Redis-compatible, in-memory key-value server written from scratch in Java 21, with no
dependencies. It speaks the real Redis protocol (RESP2), so the official redis-cli and
redis-benchmark tools work against it unchanged.
- RESP2 protocol parser that streams commands from the socket, so it correctly handles commands split across TCP packets and thousands of pipelined commands in one packet. Inline (plain-text) commands work too.
- 30+ commands:
GET SET (EX PX EXAT PXAT NX XX KEEPTTL GET) SETNX MSET MGET DEL UNLINK EXISTS INCR DECR INCRBY DECRBY APPEND STRLEN EXPIRE PEXPIRE PEXPIREAT PERSIST TTL PTTL TYPE KEYS DBSIZE FLUSHALL FLUSHDB PING ECHO SELECT INFO CONFIG GET QUIT - Key expiry the way Redis does it: lazily when a key is read, plus an active expiry cycle 10 times a second that samples keys with a TTL and deletes expired ones.
- Append-only file (AOF) persistence with fsync every second. Relative TTLs are logged as absolute timestamps so a restart never extends a key's life, and a file damaged by a crash mid-write is repaired automatically by truncating the partial command.
- Concurrency: one Java 21 virtual thread per client; lock-free reads on a
ConcurrentHashMap; atomic read-modify-write (INCR,APPEND,SET NX). With persistence on, 64 striped locks keep the AOF in the same order the writes were applied. - Pipelining-aware output: replies are buffered and flushed once per batch of pipelined commands, not once per command.
Measured in CI with the official redis-benchmark on a standard GitHub Actions ubuntu-latest
runner, after JIT warm-up. Real Redis was run on the same runner, with the same settings, for
comparison. The benchmark client shares the machine with the server.
| Scenario | This server | Redis (same runner) | Ratio |
|---|---|---|---|
| SET, 50 clients | ~49,000 req/s | ~73,000 req/s | 67% |
| GET, 50 clients | ~49,000 req/s | ~72,000 req/s | 69% |
| SET, 50 clients, pipeline 16 | ~663,000 req/s | ~1,053,000 req/s | 63% |
| GET, 50 clients, pipeline 16 | ~672,000 req/s | ~1,187,000 req/s | 57% |
| GET, 1,000 concurrent clients | ~44,000 req/s | ~71,000 req/s | 62% |
Numbers vary by machine and from run to run. Reproduce them with bash bench/run.sh; CI runs it
on every push, shows the table in the job summary and uploads it as the benchmark-results artifact.
39 JUnit 5 tests run in GitHub Actions on every push:
- Protocol: pipelined input, byte-at-a-time delivery, binary-safe values, malformed input.
- Data store: TTL edge cases with an injectable clock, active expiry, NX/XX/KEEPTTL, overflow.
- Concurrency: 64 threads × 10,000
INCRwith no lost updates; exactly one winner forSET NX. - Persistence: restart recovery, TTLs surviving downtime correctly, crash-damaged file repair.
- End-to-end over TCP: 10,000 pipelined commands answered in order, 200 concurrent clients, a protocol error on one connection not affecting others, data surviving a server restart.
mvn verify # build, run all tests, write a coverage report to target/site/jacocomvn -q package -DskipTests
java -jar target/codecrafters-redis.jar --port 6379 --appendonly yes --dir ./data
redis-cli SET greeting hello EX 60
redis-cli GET greetingdocker build -t java-redis-server .
docker run -p 6379:6379 -v redis-data:/data java-redis-serverThe container runs as a non-root user with persistence enabled, storing data in the /data volume.
It also honours a PORT environment variable, which hosting platforms set automatically.
src/main/java/
├── Main.java # entry point and command-line options
└── redis/
├── RedisServer.java # TCP accept loop, virtual thread per client, background tasks
├── RespParser.java # streaming RESP2 parser
├── Resp.java # reply encoding
├── CommandHandler.java # command dispatch, AOF logging, write ordering
├── DataStore.java # concurrent key-value store with expiry
├── AppendOnlyFile.java # AOF persistence and crash recovery
└── Glob.java # KEYS pattern matching
src/test/java/redis/ # unit, concurrency, persistence and integration tests
bench/run.sh # redis-benchmark script
.github/workflows/ci.yml # tests, benchmark and Docker build on every push
Started as a CodeCrafters "Build your own Redis" challenge, then extended with persistence, expiry, pipelining, tests and benchmarks.