4.4.24 · D4Databases

Exercises — NoSQL — document (MongoDB), key-value (Redis), column (Cassandra), graph (Neo4j)

3,686 words17 min readBack to topic

Figure — NoSQL — document (MongoDB), key-value (Redis), column (Cassandra), graph (Neo4j)
Figure 1 — N = 3 replicas; the coral write-set and dashed mint read-set share node 2, so the quorum read cannot miss the latest write.


Level 1 — Recognition

"Which family is this?" No calculation, just matching a shape to a model.

Recall Solution
  1. Key-value (Redis). Why: you always know the exact key, you need speed, and you need a TTL (time-to-live = auto-expiry). That is exactly the key-value sweet spot — no query engine needed.
  2. Document (MongoDB). Why: the fields differ per item, so a rigid table with fixed columns fights you. A document stores each item's own shape.
  3. Graph (Neo4j). Why: "the relationships ARE the data", and multi-hop traversal is the query. Graph DBs store direct neighbour pointers so each hop is cheap.
  4. Wide-column (Cassandra). Why: extreme append-heavy writes, partitioned by sensor_id, sorted by time — a textbook wide-column table.
Recall Solution
  • sensor_id is the partition key → it decides which node stores the row (Cassandra hashes it).
  • ts is the clustering key → it decides the sort order of rows inside that one partition. So all readings for one sensor live together on one node, already sorted by time — perfect for "give me sensor 7's readings from newest to oldest".

Level 2 — Application

Apply one formula or one rule to a concrete input.

Recall Solution

The rule from the parent note: a read is guaranteed to see the latest write iff . Since is false, the condition fails → no guarantee. The 3 nodes that hold the fresh write and the 2 nodes we read could be completely disjoint (3 + 2 = 5 covers all nodes with no overlap), so the read may return stale data.

Recall Solution

We need . The smallest integer is . Check: . ✅ Now the 3 written nodes and 3 read nodes must share at least node — pigeonhole guarantees the overlap.

Recall Solution

Guaranteed overlap node. That single shared node holds the newest value; a version timestamp then lets the read pick the latest among the 2 it saw. This is why on is the classic "strong on demand" setting.

Figure — NoSQL — document (MongoDB), key-value (Redis), column (Cassandra), graph (Neo4j)
Figure 2 — Left: shares node 2 (strong). Right: can be disjoint (eventual).


Level 3 — Analysis

Compare options, explain a tradeoff, reason about a failure.

Recall Solution

Both are strongly consistent ( each). The difference is which operation you make cheap/robust:

  • : balanced. A write needs 2 of 3 up; a read needs 2 of 3 up. Tolerates 1 node down for both reads and writes.
  • : writes must reach all 3 nodes → if any node is down, writes stop. But reads need only 1 node → reads are super fast and highly available. Pick for a read-heavy system where writes are rare and you want the cheapest possible reads. Pick when you can't afford writes to fail whenever one node blinks.
Recall Solution
  • SQL: each hop is a JOIN that must search an index for matching rows across the entire friends table. In a B-tree index a lookup costs for rows, and it repeats for every node in the current frontier; the frontier itself multiplies by the branching factor each hop. Cost scales with the total table size and depth — it explodes (this is the repeated-traversal idea behind Graph Traversal Algorithms (BFS/DFS): a breadth-first expansion, but paying an index search at every single step).
  • Graph DB (index-free adjacency): "index-free" means the DB does not look a neighbour up in a global index at all. Each node record stores direct pointers (memory/disk addresses) to its neighbour records — like a linked list baked into the node. Following one edge is : dereference a pointer, no search. So 3 hops = 3 cheap pointer-chasings from the local frontier, touching only the reachable nodes, never the whole dataset. Figure 3 draws this contrast.

Figure — NoSQL — document (MongoDB), key-value (Redis), column (Cassandra), graph (Neo4j)
Figure 3 — SQL searches an index per hop (left); graph DB follows direct neighbour pointers, per hop (right).

Recall Solution
  • Payments ledger = CP (Consistency + Partition-tolerance). It sacrifices Availability during the partition (refuses to answer) so it never returns/commits a wrong balance. Correctness beats uptime here.
  • Like counter = AP (Availability + Partition-tolerance). It sacrifices Consistency briefly — two partitions might undercount — but stays up and eventually converges when links heal. A momentarily wrong like count is harmless. This is the real CAP choice: not "which of 3 to drop", but CP vs AP while partitioned (see CAP Theorem).

Level 4 — Synthesis

Combine multiple ideas into a design.

Recall Solution
Need Family One-line why
(a) session/cart Redis (key-value) fetched by one session key, needs speed + TTL
(b) catalog MongoDB (document) per-item flexible fields (book vs shirt) → documents
(c) click firehose Cassandra (wide-column) huge append-only writes, partition by user_id, no master
(d) recommendations Neo4j (graph) it's bought → product ← bought traversal
The meta-lesson: polyglot persistence — one app, several databases, each matched to its access pattern. There is no single "best" NoSQL.
Recall Solution
  • Read win: fetching a post + its comments is one document read — no join, one disk locality, fewer round-trips. Ideal when you almost always show post and comments together.
  • Write cost: if the same user's display-name is duplicated across many embedded comments, renaming that user means touching many documents (updates to duplicated data are painful). The document can also grow unbounded (huge comment threads).
  • Keep separate when: comments are enormous, edited independently, or queried on their own ("all comments by user X across all posts"). Then reference by id instead of embedding. This is the SQL-vs-document tradeoff (compare SQL Relational Databases).

Level 5 — Mastery

Design under constraints, defend against edge cases, prove a claim.

Recall Solution

Step 1 — consistency (i): need . Try : ✅. Overlap node guaranteed shared → strong. Step 2 — survive one DC down (ii): with one DC gone, only 2 replicas remain.

  • Writes need acks → 2 nodes still alive → writes survive ✅.
  • Reads need → 2 nodes still alive → reads survive ✅. So meets both (i) and (ii) for a single DC failure. Step 3 — the CAP catch: this only holds while at most one DC is down and the survivors can still talk. If a partition isolates every DC from the others (each alone with 1 replica), then no side can gather 2 acks → to keep consistency (i) the system must refuse requests (lose availability). That is CAP biting: you chose CP, so under a severe partition you sacrifice availability. You cannot have strong consistency and full availability and partition-tolerance at once (see CAP Theorem, BASE vs ACID).
Recall Solution

Suppose, for contradiction, the write set (size ) and read set (size ) are disjoint (share no node). Then together they occupy distinct nodes, and all of them must fit inside the total replicas: But we assumed . Contradiction. Therefore they cannot be disjoint → they share at least That shared node witnessed the latest write, so the read (which touched it) sees the newest value once a version/timestamp breaks ties. ∎

Recall Solution
  • (a) Rate-limit counter → Redis (key-value). INCR is atomic and microsecond-fast; a tiny transient miscount under partition is acceptable (lean AP). Fetched by one known key → key-value.
  • (b) Audit log → Cassandra (wide-column). Append-only, write-heavy, partition by account_id, cluster by ts → immutable and read back in time order. Its LSM write path (see Database Indexing (B-tree, LSM)) makes the firehose cheap.
  • (c) Fraud rings → Neo4j (graph). Detecting cycles of money movement is a graph traversal; the same self-join explosion from L3.2 would cripple SQL here.
  • (d) Customer profiles → MongoDB (document) run at a strong quorum, or an ACID relational store. This is the CAP-sensitive choice. "Strong per-account consistency" means you run it CP: set — e.g. (overlap ) — and accept that a severe partition may make you reject some profile updates (lose availability) rather than risk two conflicting balances. Mastery point: each subsystem gets its own consistency stance — (a) leans AP for speed, (d) is CP for correctness. You do not apply one global CAP choice to the whole app; you match model and consistency to each access pattern (compare BASE vs ACID).