Kairo is a lightweight, self-healing distributed cache engine built in pure Java 21 without external web frameworks (no Spring, Netty, or Tomcat).
It implements consistent hashing, asynchronous replication, read fallback, and automatic node failure detection using standard Java concurrency primitives and the JDK built-in HTTP server (com.sun.net.httpserver.HttpServer).
The challenge in distributed systems isn't just storing data—it's knowing what to do when nodes fail, network partitions occur, and clocks skew. Kairo solves this by providing a decentralized, AP (Available/Partition-Tolerant) architecture that proactively detects failures and automatically routes around them with zero dropped read requests.
Watch the system seamlessly handle an abrupt SIGKILL of a primary storage node without dropping a single request!
The top pane shows proactive failure detection and dynamic routing; the bottom pane shows a client hammering the cluster with zero hard failures.
(Click the link above to view the full terminal recording.)
During a 50-second continuous load test (~30 req/s), one of the three physical nodes was abruptly killed via SIGKILL.
- Total Requests Executed:
1,473 - First-Try Success:
1,470(99.79%) - Retry-Recovered:
2(Caught in the exact sub-second crash window; recovered on the first retry via fallback replicas) - Hard Failures:
0(Zero dropped requests) - Detection-to-Reroute Time:
< 1 second(Proactive TCPECONNREFUSEDinterception bypassed the 4.5s heartbeat threshold, updating cluster health maps almost instantly).
Kairo uses an MD5-based consistent hash ring with 150 virtual tokens per physical node. This guarantees deterministic routing and even data distribution without requiring a central coordinator.
graph TD
Client((Client)) --> |"POST /cache/key-1"| N1
Client --> |"GET /cache/key-2"| N3
subgraph Hash Ring
N1[Node 1: Port 8081]
N2[Node 2: Port 8082]
N3[Node 3: Port 8083]
N1 -.- N2
N2 -.- N3
N3 -.- N1
end
N1 -->|"1. Write Local (Owner)"| S1[(Local Store)]
N1 -->|"2. Async Replicate"| N2
N2 -->|"3. Write Replica"| S2[(Replica Store)]
Kairo's most distinctive contribution is its proactive failure detection and dynamic read fallback. Nodes ping each other every 1.5 seconds. If a node dies, the cluster routes around it automatically.
sequenceDiagram
participant C as Client
participant N1 as Node 1 (Gateway)
participant N2 as Node 2 (Dead Owner)
participant N3 as Node 3 (Alive Replica)
Note over N1,N3: Node 1's Failure Detector marks Node 2 as DEAD
C->>N1: GET /cache/my-key
N1->>N1: Hash resolves to Node 2
N1--xN2: Checks health (Node 2 is DEAD)
N1->>N3: Forward GET (Replica Fallback)
N3-->>N1: Returns replicated value
N1-->>C: 200 OK (Zero failed reads!)
- Language: Pure Java 21, zero frameworks. Proves a strong grasp of core concurrency (
ConcurrentSkipListMap,ScheduledExecutorService). - Protocol: HTTP/REST via
com.sun.net.httpserver. Simplifies cross-node RPCs and debugging. - Consistency Model: AP (Available and Partition-Tolerant) by default, but allows per-request Quorum consistency (
?consistency=quorum). - Replication Factor:
RF=2. Each key is stored on its primary owner and one subsequent replica. - Eviction: Dual strategy. Lazy eviction on stale reads + background sweeper daemon, strictly bounded by a max capacity LRU policy.
Kairo makes explicit architectural trade-offs:
- AP Over CP (Default): By default, Kairo prioritizes Availability and Partition Tolerance. Writes return immediately (
201 STORED) while asynchronous replication happens in the background. There is a small window of eventual consistency where a replica might serve a404for a few milliseconds after the primary acknowledges a write. - The Quorum Mode Alternative: For users who require strict consistency, Kairo supports per-request quorum consistency via
?consistency=quorum. We measured the physical cost of this trade-off using Apache Benchmark (ab):- Default (Async/Eventual) SET:
~1.3 msaverage per request. - Quorum (Synchronous) SET:
~4.3 msaverage per request. Quorum mode inherently takes ~3x longer because it must absorb the full HTTP round-trip latency to the replica node before acknowledging the client.
- Default (Async/Eventual) SET:
- Clock Skew Assumption: Expiry (
TTL) logic uses absolute epoch timestamps (milliseconds). While this prevents bugs associated with recomputingnow + ttlat each network hop, it assumes clock synchronization across nodes (which holds true in a single-host Docker environment). In real multi-DC production systems, you'd need NTP or TrueTime (Spanner). - Eventual View Convergence: Health checks operate independently without distributed consensus (like Raft). Node health views converge eventually but aren't strictly consistent at every microsecond.
Building a distributed system from scratch reveals edge cases you don't find in tutorials. Here are three major bugs I encountered and resolved:
- The Rebalancing Data Gap: Bug: When a dead node rejoined the cluster (
DEAD -> ALIVE), its virtual tokens were immediately re-inserted into the ring. However, its local memory was completely empty! Clients requesting keys suddenly got404 Not Foundbecause the ring routed them to the newly alive (but empty) node instead of the stand-in replicas. Fix: I implemented a Pull-Based Bulk Migration protocol. Before a node starts serving client traffic, it queries its peers for its assigned key range, absorbing the payloads from the stand-in nodes in bulk. - Infinite Forwarding Loops: Bug: In early iterations, if Node 1 forwarded a request to Node 2, and Node 2 thought Node 1 was actually the owner (due to brief topology disagreements), they would bounce the HTTP request back and forth until the JVM threw a
StackOverflowErroror socket exhaustion. Fix: I implemented explicitX-Kairo-Forwarded: trueandX-Kairo-Replication: trueHTTP headers. A node instantly rejects forwarding a request that has already been forwarded once, guaranteeing a maximum of 1 network hop. - Quorum Read Split-Brain: Bug: When implementing Quorum Read-Repair, if a non-owner node received the
?consistency=quorumrequest, it forwarded the internal query to the owner. The owner, seeing an internal query without a forwarded header, forwarded it back, resulting in a node comparing its own timestamp against itself. Fix: Added strictX-Kairo-Forwardedtagging to internal Quorum checks to ensure nodes respected internal read-repair queries without triggering the standard consistent-hash forwarding path.
Launch a fully functional 3-node cluster locally using Docker Compose with zero manual steps:
# 1. Build and start the 3-node cluster in the background
docker compose up -d --build
# 2. Verify the cluster is healthy and nodes see each other
curl http://localhost:8081/status# Write a value with a 60-second TTL
curl -X POST -d "Hello Distributed World" "http://localhost:8081/cache/my-key?ttl=60"
# Read it back from ANY node (Kairo routes it automatically)
curl http://localhost:8082/cache/my-key
# Check which nodes actually hold the data
curl "http://localhost:8081/ring/owner?key=my-key"