Design Your Own NoSQL Store — build it, don't read it
This is not a "how NoSQL works" explainer — you already have that. This is a design session: you build a distributed key-value / wide-column store yourself, piece by piece, in front of your own mutual fund / IPO / demat context. At every step, decide first — the reveal is a reference design to compare against, not "the" answer.
10 segments~3–4 hrs, self-pacedFormat: build → commit → reveal → compareGoal: your own defensible design, not recall
step 01
Functional & non-functional requirements
Before you design a single component, you have to be able to say — in numbers, not adjectives — what this store is for and what it costs to be wrong about.
Build it yourself first
Write your own 5–7 bullet FR list for a NoSQL store you'd build to sit under your MF ledger / IPO allotment / demat data. Then commit to three NFR numbers: sustained write throughput, p99 read latency, and where you land on the consistency/availability line. Finally, write one sentence on what you're deliberately scoping OUT and why that exclusion matters as much as anything you included.
Q1What's the single opening sentence your FR list should start with — the one that pins down exactly what this store is for?reveal
Something like: "A partition-tolerant, horizontally-scalable store for high-write-volume, key-addressable financial event data, where a single record's latest state must always be reconstructable even if a node holding it disappears."Why this framing: it commits to three things immediately — write-heavy, key-addressable (not relational), and durability-over-features — which rules out a large class of "nice to have" requirements before you even list them.
Q2Reveal: a reference FR list for this store, grounded in your platform.reveal
Accept a write for any key without requiring a schema migration.
Guarantee a write, once acknowledged, survives the loss of any single node.
Serve point reads by key with a bounded, predictable latency envelope.
Serve ordered range reads within one partition (e.g. one customer's transactions by time) without a full scan.
Support at least one secondary lookup path (e.g. by ISIN or email) with an explicitly stated staleness bound.
Allow the cluster to grow or shrink without a maintenance-window write pause.
Expose, per-write, a caller-chosen consistency level rather than one fixed global setting.
Why bullet 7 matters most: it's the one FR that prevents every future team from silently inheriting the wrong consistency guarantee for their data — which is exactly the incident pattern that shows up in mature systems.
Q3Reveal: NFR targets, with real numbers and the reasoning behind each one.reveal
Sustained writes
15K/s steady-state
Burst writes
80K/s NFO / IPO open
Read p99
15ms point read
Write p99
40ms quorum ack
Availability target
99.95% writes accepted
Durability
RF=3 cross-AZ
Why the burst number matters more than the steady-state one: a store sized only for average load is precisely the store that falls over on the one day — an NFO open, an allotment run — where the business actually needs it most. NFR numbers that ignore your worst known day aren't targets, they're wishes.
Q4What should you explicitly scope OUT, and why does saying no here matter more than usual for this kind of system?reveal
Scope out: ad-hoc multi-key ACID transactions across partitions, a general-purpose query language, and strict global ordering across unrelated keys.Why: each of those three, if included, forces a coordination mechanism (distributed locking, a query planner, a global sequencer) that fights every partitioning and replication decision you're about to make in segment 2. A NoSQL store that tries to be "SQL but distributed" usually ends up being neither fast nor simple — scoping these out up front is what keeps the rest of this design honest.
step 02
Scope & the core design decision
Two decisions here shape every segment that follows: how data is split across nodes, and how each split survives a node dying. Get these wrong and nothing downstream can save you.
Build it yourself first
Commit, in writing, to: (a) hash-based or range-based partitioning, and (b) leader-based or leaderless quorum replication. Justify each choice directly against your own NFR numbers from segment 1 — not against what's "modern."
Q5Given your FR list (point reads by key, range reads within one partition, burst writes on hot keys) — hash or range partitioning, and why?reveal
Consistent hashing with virtual nodes for the partition key (e.g. customer_id or scheme_id), with data clustered/sorted by a secondary key (e.g. timestamp) within each partition.Why: pure hash partitioning gives you even spread and defuses the NFO-day hotspot from FR3/NFR-burst, while clustering within the partition still gives you the ordered range read from FR4 — you get both properties by applying them at two different levels of the key, not by picking one partitioning strategy and hoping it does both jobs.
Q6Leader-based or leaderless quorum replication — and what does that choice cost you against your 99.95% write-availability target?reveal
Leaderless, quorum-based replication (any of the N replicas for a partition can coordinate a write; success requires W acks).Why: leader-based replication makes a leader-election pause a direct hit against your write-availability NFR every time a leader dies. Leaderless removes that single point of write unavailability at the cost of pushing conflict handling onto quorum math and read repair — a cost you're explicitly willing to take on, per FR2 and FR7, because durability-under-node-loss was stated as non-negotiable.
Leader-based replication
Leaderless quorum replication (chosen)
Simple consistency story — one authoritative writer per partition
No single point of write unavailability — any replica can coordinate
Failover pause during leader election directly costs write availability
Correctness now depends on quorum math (W+R>N) and conflict resolution
Easy to reason about "the current value" — it's whatever the leader has
"Current value" is a function of which replicas you queried and when
Q7You picked leaderless. What single number, set at write-time, do you now have to expose as a first-class caller-facing decision — and why does that connect straight back to FR7?reveal
The consistency level per call — effectively, the caller's chosen W (write quorum) and R (read quorum) against your fixed replication factor N.Why: FR7 explicitly required per-call consistency rather than one global setting. Leaderless replication is the only topology of the two where that requirement is even structurally possible — a leader-based system's consistency story is baked into the topology itself, not exposed as a per-call dial.
step 03
Component breakdown
Draw the boxes before you reveal them. Every box you draw should exist because something would break if it were merged into a neighboring box.
Build it yourself first
Draw your own 5–7 box component diagram for this store. For each box, be ready to say one sentence about what breaks if you merged it into the box next to it.
Q8Why can't the coordinator's hot-path routing logic live inside the same component as the storage engine's durable write path?reveal
Because they have opposite performance and failure profiles: the coordinator must decide "which replicas, what quorum, route now" in microseconds and must be stateless enough to fail over instantly, while the storage engine must guarantee a fsync-durable write even if that takes longer under load. Merging them means a slow disk on one node now blocks routing decisions for every key that node happens to coordinate — a local disk problem becomes a cluster-wide routing problem.
+------------------+
Client->| Coordinator |--- routes by consistent-hash ring
+------------------+
| |
v v
+----------------+ +----------------+
| Storage Engine | | Storage Engine | (one per node: WAL -> memtable -> SSTable)
| (Node A) | | (Node B) |
+----------------+ +----------------+
^ ^
| |
+------------------+ +------------------+
| Replication / | | Hinted Handoff | buffers writes for a temporarily-down replica
| Quorum Manager | +------------------+
+------------------+
^
|
+------------------+ +------------------+
| Membership / | | Anti-Entropy / |
| Gossip | | Repair Manager |
+------------------+ +------------------+
Q9Why does Hinted Handoff need to be its own component rather than a feature bolted onto Replication / Quorum Manager?reveal
Because it has a fundamentally different lifecycle: Replication/Quorum Manager runs synchronously, in the write's critical path; Hinted Handoff runs asynchronously, potentially for hours, buffering writes meant for a down replica until it returns. Coupling them means a slow or full hint buffer can back-pressure onto live writes it has nothing to do with.
Q10Why does Membership/Gossip have to be a separate component from the Coordinator, even though the Coordinator needs to know cluster state to route anything?reveal
Because Gossip's job is to converge eventually across the whole cluster, while the Coordinator needs a fast, local, possibly-slightly-stale read of that state to route a single request in microseconds. If routing had to wait for gossip convergence on every call, you'd trade routing latency for membership freshness — the Coordinator instead reads a locally-cached view that Gossip updates in the background.
step 04
Low-level API design
An API surface is where blast radius gets decided. A data-plane call and a control-plane call that "happen to use the same port" are not the same API, no matter how similar they look.
Build it yourself first
Write your own 5–6 endpoints as method + path + one-line purpose + caller + payload notes. Before revealing, decide: should any of these be reachable by an app service directly, or only by another cluster node / an operator? Scope authorization by blast radius, not by ownership.
PUT/kv/{partition_key}
Write a value under a key, at caller-chosen consistency level.
Caller app servicePayload value, consistency_level (ONE/QUORUM/ALL), client_timestampIdempotency last-write-wins by (timestamp, node_id) tiebreak — safe to retry
GET/kv/{partition_key}
Read a value at caller-chosen consistency level.
Caller app servicePayload consistency_levelIdempotency naturally idempotent — read has no side effect on the primary write path
POST/internal/replicate/{partition_key}
Coordinator pushes a write to a specific replica node.
Caller coordinator node onlyPayload value, vector_clock/timestamp, coordinator_idIdempotency replica applies by timestamp comparison — duplicate delivery is a no-op
POST/internal/hinted-handoff/deliver
Deliver buffered writes to a replica that just rejoined.
Caller the node that was holding the hints, on peer-rejoin detectionPayload batch of (key, value, timestamp) hints, hint_ageIdempotency same last-write-wins rule applies — stale hints lose to newer direct writes
POST/internal/repair/merkle-diff
Exchange Merkle tree summaries with a peer replica to find drift.
Caller Anti-Entropy/Repair Manager, on its own schedulePayload tree hash summary for a key rangeIdempotency comparison-only; no state mutation until a follow-up sync call
Q11Why should authorization for these APIs be scoped by blast radius rather than simply "who owns this service"?reveal
Because two APIs can be "owned" by the same team and still have wildly different blast radii — PUT /kv/{key} can only ever corrupt one key, while a leaked credential for /internal/replicate could let a caller impersonate a coordinator and write inconsistent data cluster-wide. Ownership tells you who to page; blast radius tells you how tightly to lock the door — and those are frequently different answers for APIs sitting right next to each other in the same service.
Q12Why does the PUT endpoint accept a client_timestamp instead of always using server-received time?reveal
Because last-write-wins conflict resolution in a leaderless system needs a timestamp that reflects when the write was decided, not when it happened to arrive at whichever replica processed it — server-received time would make conflict resolution dependent on network jitter and routing luck rather than actual write order.Why this is a real risk, not a theoretical one: if two concurrent writes to the same key take different network paths, server-received time could resolve the conflict backwards from what the caller intended — client_timestamp (with a documented clock-skew tolerance) keeps that decision anchored to intent.
step 05
Data model & sizing
Numbers force honesty. A schema that looks elegant on a whiteboard can be a very different animal once you multiply it by 80+ microservices' worth of write volume.
Build it yourself first
Sketch your own schema for one concrete table (e.g. MF ledger entries) and estimate row count, sustained write throughput, and 1-year storage size. Separate, explicitly, what's durable config (must survive restarts, changes rarely) from ephemeral/in-memory derived state (can be rebuilt, changes constantly) — and write one sentence on why conflating the two is a design mistake.
Q13Reveal: a reference schema sketch and sizing estimate for one table.reveal
Why the clustering key is txn_timestamp DESC, not just timestamp: the dominant read pattern is "most recent transactions first" for a customer's statement view — sorting descending at the storage layer means that query is a sequential prefix scan, not a full-partition scan followed by an application-side sort.
Q14In this schema, what's durable config vs. ephemeral/in-memory derived state — and why would conflating them be a mistake?reveal
Durable config: the ring/partition map, replication factor, schema/column definitions. Ephemeral/derived state: the memtable contents, the read cache, gossip's locally-cached membership view — all rebuildable from the WAL or from peers.Why conflating them is a mistake: if you replicate ephemeral state (like a read cache) with the same durability guarantees as durable config, you're paying real replication cost to protect data that was never supposed to need protecting — it should have been rebuilt cheaply from source instead. The inverse mistake — treating durable config as rebuildable — is worse: lose the ring topology without a durable backup, and you don't know which node owns which data anymore.
Q15The gut-check: is this actually a "big data" problem, or could a single well-tuned relational instance have handled it?reveal
At 15K writes/sec sustained with 180GB/year growth, a single powerful relational instance with read replicas could plausibly handle this — the honest answer is: the burst number (80K/sec on NFO day) and the multi-region durability requirement are what actually force the distributed design, not the steady-state volume.Why this question matters even after you've built the whole design: it's the question a sharp director asks at the review — "did we need this complexity, or did we default to it because it's the modern answer?" Being able to point at the specific number that justified it is the difference between an engineering decision and a trend follow.
step 06
Core algorithm — ring lookup & quorum write path
One mechanism, in real pseudocode, not prose. This is the piece an interviewer will actually make you write on a whiteboard.
Build it yourself first
Write pseudocode for (a) the consistent-hash ring lookup — the main data structure — and (b) the coordinator's quorum write path — the per-operation logic. Cover thread-safety for concurrent writes to the same key, and explicitly answer: what happens when the coordinator cannot reach enough replicas to satisfy W?
Q16Reveal: ring lookup pseudocode — the main data structure.reveal
struct Ring:
sorted_positions: list<(hash_value, virtual_node_id, physical_node)>
function get_replicas(key, N):
h = hash(key)
idx = binary_search_first_position_gte(ring.sorted_positions, h)
replicas = []
physical_seen = set()
while len(replicas) < N:
(pos, vnode, node) = ring.sorted_positions[idx]
if node not in physical_seen:
replicas.append(node)
physical_seen.add(node)
idx = (idx + 1) mod len(ring.sorted_positions)
return replicas
Why virtual nodes and physical_seen dedup both matter: virtual nodes spread each physical node's ownership across many ring positions, smoothing load; the physical_seen check prevents two virtual nodes belonging to the same physical machine from both counting toward your N — otherwise a "replicated" write could silently have fewer real physical copies than N implies.
Q17Reveal: coordinator's quorum write path pseudocode, including the "can't reach W replicas" edge case.reveal
function coordinate_write(key, value, W, N):
replicas = get_replicas(key, N)
write_ts = current_client_timestamp()
acked = 0
hinted = []
for node in replicas: # fan out concurrently, not sequentially
try:
send_replicate(node, key, value, write_ts, timeout=WRITE_TIMEOUT)
acked += 1
except Timeout or NodeDown:
hinted.append(node) # buffer via hinted handoff, don't block
if acked >= W:
return SUCCESS
else:
# sloppy quorum: write to next available healthy node in the ring
# instead of the "correct" but unreachable one, plus a hint for the real owner
fallback_node = next_healthy_node_not_in(replicas)
send_replicate(fallback_node, key, value, write_ts)
acked += 1
if acked >= W:
return SUCCESS
else:
return FAIL_INSUFFICIENT_QUORUM # client must decide: retry, or accept the write is not guaranteed
Why fan out concurrently, not sequentially: sequential replication would multiply write latency by N; concurrent fan-out means write latency is bounded by the slowest of the first W replicas to respond, not the sum of all N.Why sloppy quorum exists at all: without it, a single temporarily-down replica in a small ring could make writes to its owned keys fail outright even though the cluster has plenty of healthy capacity elsewhere — sloppy quorum trades strict ownership for availability, exactly the tradeoff FR2/NFR-availability asked for.
Q18Two writes to the same key arrive at the coordinator within a few milliseconds of each other. How does your pseudocode above guarantee thread-safety without a global lock?reveal
It doesn't need a lock, because each write is independently routed to the same N replicas and each replica resolves the conflict locally by comparing incoming timestamp against its currently stored value — last-write-wins, applied per-replica, per-key, on arrival.Why this avoids the coordination bottleneck a global lock would introduce: a lock would force every write to the same key through a single serialization point — exactly the kind of hotspot segment 3's component split was designed to avoid. Pushing conflict resolution to the replica, at apply-time, means concurrent writes to the same key are handled in parallel across replicas and simply converge to the same final answer via the same deterministic rule everywhere.
step 07
Supporting subsystem — anti-entropy & repair
Quorum writes tolerate a temporarily-down replica, but replicas that stayed down longer than a hint's TTL, or that missed writes some other way, need a background process to catch back up. This is that process.
Build it yourself first
Design the anti-entropy repair loop's logic precisely — when does it run, what does it compare, what does it do with a diff. Then decide: should repair scheduling be centralized (one coordinator service tells nodes when to repair) or distributed (each node schedules its own repairs against its peers)? Justify your choice.
Q19Reveal: the repair loop's logic, step by step.reveal
every REPAIR_INTERVAL (e.g. 24h), per partition range, per node:
for each peer replica of this range:
local_tree = build_merkle_tree(local_range_data)
peer_tree = request_merkle_tree(peer, range)
diff_leaves = compare_trees(local_tree, peer_tree) # only mismatched sub-ranges
for leaf in diff_leaves:
local_data = read_range(leaf.range)
peer_data = request_range(peer, leaf.range)
merged = resolve_by_timestamp(local_data, peer_data) # same LWW rule as writes
apply_missing_or_newer(local, merged)
stream_missing_or_newer(peer, merged)
Why compare tree hashes before comparing any actual data: full record-by-record comparison across a large partition range is prohibitively expensive in bandwidth. Comparing small hash summaries first, and only drilling into the specific sub-ranges that differ, turns an O(all data) operation into something close to O(actual drift) — most of the tree usually matches and gets skipped entirely.
Q20Centralized repair scheduling or distributed — and what's the actual tradeoff?reveal
Distributed — each node schedules its own repairs against its peer replicas, staggered to avoid overlapping load.Why: a centralized repair coordinator becomes exactly the kind of single point of failure this whole design has been avoiding since segment 2 — if it goes down, repair silently stops cluster-wide with no local fallback. Distributed scheduling costs you a bit of coordination complexity (staggering to avoid every node repairing at once and saturating the network) but means no single component's failure can silently disable the entire cluster's self-healing.
Q21Why does repair use the exact same last-write-wins rule as the live write path, instead of its own conflict logic?reveal
Because using a different conflict rule for repair than for live writes would mean the "corrected" state after repair could disagree with what a live quorum read would have returned for the same conflicting writes — two different subsystems producing two different answers to the same question. Reusing one deterministic rule everywhere is what makes "eventually consistent" mean something concrete: eventually, every path converges to the identical answer, not just an answer.
step 08
Failure handling & the node state machine
Every state mentioned so far — healthy, unreachable, catching up — belongs to one state machine. Draw it before you reveal it; a state machine you can't draw is one you haven't actually designed.
Build it yourself first
Draw the full node state machine yourself, pulling in every state implied by earlier segments (healthy, suspected/unreachable, down, rejoining/replaying hints, draining/decommissioning). Then find the "stuck forever" risk: is there any state your diagram allows a node to enter and never leave without external intervention? Fix it with a timeout.
Q22Reveal: the reference state machine.reveal
gossip missed heartbeats
HEALTHY -------------------------> SUSPECTED
^ | \
| heartbeat resumes | \ SUSPECT_TIMEOUT elapses
+-------------------------------------+ v
DOWN
|
node process restarts / rejoins
v
REJOINING
(replaying hinted handoff,
catching up via repair)
|
caught up within threshold
v
HEALTHY
any state --(operator triggers decommission)--> DRAINING --(data streamed off)--> REMOVED
Why SUSPECTED is a separate state from DOWN, not the same thing: a single missed heartbeat is often a transient network blip, not a real failure. Treating it as DOWN immediately would trigger unnecessary hinted handoff and repair traffic for every minor GC pause or network hiccup — SUSPECTED is a probation period that lets the node prove it's still alive before the cluster pays the cost of treating it as gone.
Q23Where's the "stuck forever" risk in this state machine, and what guarantees an eventual exit?reveal
Two places: SUSPECTED could theoretically sit forever without a bound, and REJOINING could hang forever if hint replay or repair never actually finishes catching the node up.The fix: SUSPECTED has an explicit SUSPECT_TIMEOUT that forces a transition to DOWN, guaranteeing the cluster always eventually treats an unresponsive node as gone rather than leaving it in limbo indefinitely. REJOINING has an analogous catch-up deadline — if a node can't finish replaying hints and repairing within a bounded window, it's demoted back to DOWN rather than serving reads from a partially-caught-up state indefinitely, which would silently violate your consistency guarantees.
Q24Where does retry/idempotency from the client's side plug into this state machine?reveal
At the coordinator's FAIL_INSUFFICIENT_QUORUM outcome from segment 6 — if a write returns that failure because replicas were SUSPECTED or DOWN at the time, the client's retry is safe to send again because every write carries the same client_timestamp and last-write-wins semantics from segment 4. A retried write either lands as the first successful attempt, or loses harmlessly to whichever attempt's timestamp actually won — retries are safe precisely because the conflict-resolution rule was made deterministic back in segment 6, not because of anything new in the state machine itself.
step 09
Scaling the store itself
Not the backend system this store protects or serves — this store, as a system in its own right. How does it keep many instances of itself roughly in sync without becoming its own bottleneck?
Build it yourself first
Explain how gossip-based membership stays cheap as node count grows, and where it stops being cheap. Then write one concrete metric — tied to your segment-1 NFRs — that would tell you it's time to add nodes to this cluster.
Q25Gossip is O(1) messages per node per round, gossiping to a few random peers. Why does convergence time still grow as the cluster grows, and where does that become a real problem?reveal
Because convergence time for a rumor to reach every node scales roughly logarithmically with cluster size (each round roughly doubles the informed population) — cheap per-node, but the total time to full convergence still grows as the cluster grows, and it becomes a real problem once membership-change events (a node joining, dying, or being suspected) start happening faster than the cluster can converge on the previous change.Why this eventually caps how large one cluster should get: past a few hundred to low thousands of nodes, membership churn can outpace convergence, and the cluster spends more time disagreeing about its own topology than serving requests — the practical fix is usually sharding into multiple independently-gossiping clusters rather than growing one cluster indefinitely.
Q26Reveal: a concrete "time to scale out" metric, tied explicitly back to a segment-1 NFR.reveal
Sustained write p99 exceeding the 40ms NFR target for more than 10 minutes, driven by quorum-wait time rather than network latency.Why quorum-wait specifically, not just raw p99: a write p99 breach caused by network latency to a specific region is a routing/infra problem, not a capacity problem — scaling out wouldn't fix it. A breach caused by replicas themselves taking longer to ack under load is a genuine sign each node is doing more work than it should, which is exactly what adding nodes (and rebalancing partitions across them) is meant to relieve. The metric has to isolate cause, not just symptom, or you'll scale out for the wrong reason.
Q27When you do add nodes, why is the rebalancing operation itself a risk to the same write-latency NFR you're trying to protect?reveal
Because rebalancing means streaming partition data to the new node's virtual positions while production traffic keeps flowing — that streaming competes for the same disk and network I/O the live write and repair paths need, temporarily worsening the exact latency metric that triggered the scale-out decision.Why this has to be planned, not automatic: rebalancing throughput should be rate-limited below whatever headroom currently exists, the same way segment 6's write path avoided a global lock — trading a longer rebalance window for a smaller live-traffic impact, rather than rebalancing as fast as possible and risking the exact latency spike you were trying to fix.
step 10
End-to-end assembly
Last exercise. No reveal will tell you if your own design actually holds together — only your own segment-1 requirements can do that.
Build it yourself first
Draw the whole system from memory, in one diagram — both the hot/data path (client write and read, through the coordinator, to replicas, to disk) and the control/config path (gossip membership, ring updates, repair scheduling) together, showing where they touch and where they're deliberately kept apart.
HOT / DATA PATH CONTROL / CONFIG PATH
Client
| PUT/GET /kv/{key} Gossip <---> Gossip <---> Gossip
v (every node, background, eventual)
Coordinator --ring lookup--> |
| (reads locally-cached locally-cached ring/membership view
| ring view, doesn't wait |
| on gossip convergence) v
v Coordinator's routing table
Replicate to N replicas (fan out)
| | |
v v v
Storage Engine (WAL -> memtable -> SSTable) <-- Anti-Entropy/Repair (background, per-node schedule)
|
v
Hinted Handoff (if a replica was unreachable) --replays into--> Storage Engine on rejoin
Q28Where do the hot path and control path deliberately NOT talk to each other synchronously, and why was that a design choice rather than an oversight?reveal
The Coordinator never blocks a live request on Gossip convergence — it always routes off a locally-cached view of cluster membership, even though that view might be a few gossip-rounds stale.Why this was a deliberate choice: it directly implements the segment-1 decision to favor write availability over perfect real-time consistency of cluster metadata — a slightly stale ring view causes at worst a sloppy-quorum fallback (segment 6), which was already designed to handle exactly this case. Making the hot path wait for control-path convergence would trade a rare, already-handled edge case for a routine tax on every single request.
Q29No answer key for this one: trace your own end-to-end diagram back against your own segment-1 FR and NFR list. Where's the gap?reveal
This one is genuinely yours to find — but the shape of the exercise is: pick each FR and NFR line from segment 1 one at a time, and point to the exact component and mechanism in your diagram that satisfies it. If you land on any requirement where you can't point to a specific box — a secondary-index staleness bound with no component actually enforcing it, an availability target with no fallback path that actually kicks in — that's not a failure of this session. That gap is the most valuable thing this exercise produces: a specific, named hole in your own design, found by you, before a staff engineer or a production incident finds it for you.