A distributed in-memory cache system built from scratch — inspired by Redis, written in TypeScript.
Fedis is a from-scratch implementation of a distributed in-memory cache — the same kind of system that powers Redis. It is not a wrapper around Redis. Every piece of it — the eviction logic, the TTL system, the persistence layer, the sync mechanism — is hand-built in TypeScript.
The goal was to deeply understand how caching systems actually work under the hood, and to build something interview-worthy that demonstrates real systems thinking.
Source → github.com/sudoKrishna/fedis
fedis/
├── backend/ # Cache engine — TCP server, REST API, eviction, TTL, persistence
├── dashboard/ # Next.js live monitor — key explorer, TTL countdown, stats
└── data/ # Snapshot files (auto-generated)
Client (REST or TCP)
│
▼
Command Parser
(GET, SET, DEL, EXPIRE, TTL, KEYS)
│
▼
Cache Engine
┌─────────────────────────────────┐
│ HashMap + DoublyLinkedList │ ← LRU eviction
│ FrequencyMap + MinFreq ptr │ ← LFU eviction
│ TTL timers (lazy deletion) │ ← Expiration
└─────────────────────────────────┘
│ │
▼ ▼
Snapshot Sync Bus
(JSON dump) (Invalidation events)
All data lives in memory. get, set, delete, has — O(1) operations via a plain Map. TTL (time-to-live) is supported per key — keys expire lazily on access, so there is zero timer overhead for the common case.
When the cache reaches capacity, the Least Recently Used key is evicted. Implemented with a HashMap + DoublyLinkedList — both get and set are O(1). Moving a node to the head of the list on every access keeps the MRU/LRU order maintained without sorting.
An alternative eviction policy — evicts the Least Frequently Used key. Uses a freqMap: Map<number, Set<string>> and a minFreq pointer to achieve O(1) for all operations. The eviction policy is swappable at instantiation — LRU and LFU share a common interface.
Every key can have an optional TTL in milliseconds. On get, if Date.now() > expiresAt, the key is deleted and null is returned. No setInterval polling — lazy deletion means zero wasted cycles for keys that are never read again.
The entire cache is serialized to a JSON snapshot every 60 seconds, and on graceful shutdown. On startup, the snapshot is loaded back into memory. Expired keys are filtered at both save and load time.
Trade-off: up to 60 seconds of data loss on a hard crash. This is the same trade-off Redis makes with its default RDB persistence — acceptable for most cache use cases.
Multiple Fedis instances stay consistent via a WebSocket-based pub/sub bus. When Instance A sets or deletes a key, it publishes an INVALIDATE event. Instances B and C delete their local copy — so the next read fetches fresh data from the source of truth.
This is eventual consistency — a small window where instances disagree. For a cache, this is the right trade-off vs. the latency cost of strong consistency.
| Method | Endpoint | Description |
|---|---|---|
GET |
/cache/:key |
Get a value |
POST |
/cache/:key |
Set a value (body: { value, ttlMs? }) |
DELETE |
/cache/:key |
Delete a key |
GET |
/cache |
List all keys |
GET |
/stats |
Hit rate, eviction count, key count |
Fedis speaks RESP (Redis Serialization Protocol) over TCP. Connect with:
redis-cli -p 6380Supported commands: SET, GET, DEL, EXPIRE, TTL, KEYS
A Next.js dashboard at fedis.vercel.app shows real-time cache state — all live keys, TTL countdown per key, hit/miss ratio, and eviction stats.
- Node.js 18+
- npm
git clone https://github.com/sudoKrishna/fedis.git
cd fedis
# Backend
cd backend
npm install
npm run dev
# Dashboard (new terminal)
cd ../dashboard
npm install
npm run devBackend → http://localhost:3000
Dashboard → http://localhost:3001
docker-compose upWhy lazy TTL deletion instead of active timers?
Active timers waste memory and CPU for keys that may never be read again. Lazy deletion checks expiry on access — zero overhead, matches how Redis's volatile-lru works.
Why JSON snapshots? Human-readable, debuggable, and sufficient for this use case. Binary (like Redis RDB) offers smaller files and faster parsing — a natural next step.
Why eventual consistency for sync? Strong consistency requires a distributed lock on every write, adding latency. For a cache layer, eventual consistency is almost always the right trade-off — worst case is a stale read, not data corruption.
Every data structure that felt abstract became real here. LRU is not a concept — it is a Map and a linked list working together. Distributed systems are not theory — they are two instances disagreeing about a key's value for 12 milliseconds.
The biggest insight: performance trade-offs are not about algorithms in isolation. They are about what you are optimizing for in a specific context.
- Redis persistence docs
- RESP protocol spec
- LeetCode 146 — LRU Cache
- LeetCode 460 — LFU Cache
- Designing Data-Intensive Applications — Ch. 5
MIT
Built by @sudoKrishna