System Design
1. System Prerequisites

NoSQL Types (intro)

⏱ 0s active on this topic

In simple words

A traditional (relational) database keeps data in tables, and that starts to strain at very large scale. NoSQL databases store data in other shapes, to handle huge volumes or one specific need. There are four common shapes: key–value (a giant dictionary — very fast lookups by key), document (each record is a flexible JSON-like document), column-family (built for enormous amounts of writing), and graph (built for links between things, like friends of friends). Which one fits depends on how you will be reading and writing the data.


Why NoSQL Exists — The Problem With Relational at Scale

Relational databases were designed in the 1970s for structured,
consistent, transactional data on a single machine.

Three problems emerged at internet scale:

1. Horizontal scaling is hard
   RDBMS assumes a single node (or tightly coupled cluster).
   Adding more machines to share the load (sharding) is complex.
   JOINs across shards are nearly impossible.

2. Schema rigidity is a burden
   All rows in a table must follow the same schema.
   Adding a column to a 100M-row table → expensive ALTER TABLE.
   Different entities with different attributes are forced into the same mold.

3. Not all data is tabular
   User activity logs → better as time-series or document
   Social graph (who follows whom) → better as graph
   Session tokens → better as key-value
   Product catalog with 500 varying attributes → better as document

NoSQL = "Not Only SQL"
Not a replacement for relational databases.
A family of alternatives optimised for specific data models and scale patterns.

The Four NoSQL Families

┌──────────────────┬────────────────────────────────────┬──────────────────────────┐
│ Type             │ Data Model                         │ Examples                 │
├──────────────────┼────────────────────────────────────┼──────────────────────────┤
│ Key-Value        │ key → value (blob)                 │ Redis, DynamoDB, Memcached│
│ Document         │ key → JSON/BSON document           │ MongoDB, Couchbase, Firestore│
│ Column-family    │ key → rows with dynamic columns    │ Cassandra, HBase, ScyllaDB│
│ Graph            │ nodes + edges with properties      │ Neo4j, Amazon Neptune    │
└──────────────────┴────────────────────────────────────┴──────────────────────────┘

1. Key-Value Stores

What It Is

The simplest NoSQL model. A dictionary (hash map) at massive scale.

key   →   value (any binary blob: string, integer, JSON, image, serialized object)

"user:session:abc123"   →   {"user_id": 1, "expires": 1704067200}
"product:P01:price"     →   500
"rate_limit:ip:1.2.3.4" →   47
"lock:order:1001"       →   "processing"

Operations

SET key value [EX seconds]    → store a value (with optional expiry)
GET key                       → retrieve a value
DEL key                       → delete a value
EXISTS key                    → check if key exists
EXPIRE key seconds            → set time-to-live on existing key
INCR key                      → atomically increment a counter
SETNX key value               → set only if key does NOT exist (distributed lock)
MGET key1 key2 key3           → get multiple values in one round trip

Redis — The Primary Example

Redis is an in-memory key-value store with rich data structures:

String    → simple key → value
List      → ordered list of strings (append/prepend O(1))
Set       → unordered unique strings (intersection, union, difference)
Sorted Set → set with a score per member (leaderboards, ranges)
Hash      → key → field:value map (like a row in a table)
Bitmap    → compact binary flags (daily active users)
HyperLogLog → approximate unique count (cardinality estimation)
Stream    → append-only log (like Kafka, for event streaming)
Geo       → geospatial index (find nearby locations)

Use Cases

1. Session storage
   User logs in → create session → store in Redis with TTL
   SET session:abc123 '{"user_id":1,"role":"admin"}' EX 3600
   Every request: GET session:abc123 → O(1) session validation
   No DB query needed for session → high throughput

2. Caching
   Cache DB query results, API responses, rendered HTML
   Key = cache:users:1  Value = serialized User object
   TTL = 300 seconds
   Read: check Redis first → if miss → query DB → store in Redis

3. Rate limiting
   INCR rate:ip:1.2.3.4  → increments counter
   EXPIRE rate:ip:1.2.3.4 60
   If counter > 100 → reject request
   Atomic INCR prevents race conditions

4. Distributed locks
   SETNX lock:order:1001 "processing"
   → Returns 1 (success) if key didn't exist → lock acquired
   → Returns 0 (fail) if key exists → lock held by another process
   SET lock:order:1001 "processing" NX EX 30  → with auto-expiry (safety)

5. Pub/Sub messaging
   PUBLISH channel:notifications "order_shipped:1001"
   SUBSCRIBE channel:notifications
   Lightweight messaging between services

6. Leaderboards
   ZADD leaderboard 1500 "alice"  → sorted set: score=1500
   ZADD leaderboard 2200 "bob"
   ZRANGE leaderboard 0 9 WITHSCORES REV  → top 10 players
   ZRANK leaderboard "alice"  → alice's rank

7. Real-time analytics / counters
   INCR page_views:product:P01
   HINCRBY user:1:stats order_count 1

Limitations

- No complex queries (no SQL, no JOINs, no filtering by value)
- No relationships between keys
- Memory-bound (Redis is in-memory — data size limited by RAM)
- Not designed for primary data store (use as cache/supplement)
- No schema validation

2. Document Databases

What It Is

Stores data as semi-structured documents (usually JSON or BSON). Each document is self-contained — fields can vary between documents in the same collection.

Collection: products (like a table, but no fixed schema)

Document 1 (T-shirt):              Document 2 (Laptop):
{                                  {
  "_id": "P01",                      "_id": "P02",
  "name": "Classic T-shirt",         "name": "MacBook Pro 14",
  "price": 499,                      "price": 199900,
  "category": "clothing",            "category": "electronics",
  "sizes": ["S", "M", "L", "XL"],   "specs": {
  "colors": ["red","blue","white"],    "cpu": "M3 Pro",
  "material": "cotton"                "ram": 18,
}                                     "storage": 512,
                                      "display": "14.2-inch Liquid Retina"
                                    },
                                    "warranty_years": 1
                                  }

Same collection, completely different structure — NO schema enforcement.
A T-shirt has sizes and colors. A laptop has specs. That's fine.

MongoDB — The Primary Example

// Insert
db.products.insertOne({
  name: "Running Shoes",
  price: 2999,
  category: "footwear",
  variants: [
    { size: 8, color: "black", stock: 50 },
    { size: 9, color: "black", stock: 30 },
    { size: 8, color: "white", stock: 20 }
  ],
  ratings: { average: 4.3, count: 1250 }
});

// Query with nested field
db.products.find({
  "variants.size": 9,
  "variants.color": "black",
  price: { $lt: 5000 }
});

// Aggregation (similar to SQL GROUP BY)
db.orders.aggregate([
  { $match: { status: "DELIVERED" } },
  { $group: { _id: "$user_id", total: { $sum: "$amount" } } },
  { $sort: { total: -1 } },
  { $limit: 10 }
]);

// Indexes work like RDBMS
db.products.createIndex({ category: 1, price: 1 });
db.products.createIndex({ name: "text" });  // full-text index

Use Cases

1. Product catalog (e-commerce)
   Every product type has different attributes.
   TV: resolution, refresh_rate, panel_type
   Shoes: sizes, materials, sole_type
   Drug: dosage, contraindications, regulatory_approval
   → Document model handles attribute variety naturally
   → Relational would need EAV (Entity-Attribute-Value) pattern — very messy

2. Content management
   Blog posts, articles with varying structure
   Some have videos, some have code blocks, some have polls
   → Document per content item, nested rich content

3. User profiles
   {
     user_id: 1,
     name: "Alice",
     preferences: { theme: "dark", notifications: {...} },
     social: { twitter: "@alice", linkedin: "..." },
     addresses: [ {...}, {...} ]
   }
   → All user data in one document → single fetch for profile page

4. Event/activity logs
   Each event has different structure
   page_view: { url, duration, referrer }
   purchase: { product_id, amount, payment_method }
   → Flexible schema handles evolving event types

5. Real-time analytics / IoT data
   Sensor readings with varying metadata per device type

Limitations

- No JOINs (by design — relationships must be denormalized or referenced manually)
- No multi-document ACID transactions (MongoDB 4.0+ has them but with performance cost)
- Schema flexibility = responsibility on application to validate data
- Not ideal for highly relational data (many relationships = many manual JOINs in app layer)
- Querying nested arrays can be inefficient without careful indexing

3. Column-Family Databases

What It Is

Stores data in column families — a table-like structure where rows can have different columns and data is stored column-by-column on disk (not row-by-row).

This is the MOST MISUNDERSTOOD NoSQL type. Clarification:

NOT like SQL columns.
NOT a column-oriented analytical DB (like Redshift/BigQuery).

Cassandra data model:
  - A "table" has a partition key and optional clustering columns
  - All data for the same partition key lives on the same node
  - Rows within a partition are sorted by clustering columns
  - Designed for: write-heavy, time-series, wide rows, predictable access patterns

Cassandra — The Primary Example

Keyspace: analytics (like a database/schema)

Table: user_events

Partition key: user_id         → determines which node stores this data
Clustering key: event_time     → determines order within the partition

user_id │ event_time          │ event_type    │ metadata
────────┼─────────────────────┼───────────────┼──────────────────────────
u001    │ 2024-01-15 10:00:00 │ PAGE_VIEW     │ {"page": "/home"}
u001    │ 2024-01-15 10:05:00 │ PRODUCT_VIEW  │ {"product_id": "P01"}
u001    │ 2024-01-15 10:08:00 │ ADD_TO_CART   │ {"product_id": "P01"}
u001    │ 2024-01-15 10:15:00 │ PURCHASE      │ {"order_id": "1001"}
u002    │ 2024-01-15 09:00:00 │ PAGE_VIEW     │ {"page": "/deals"}
u002    │ 2024-01-15 09:10:00 │ SEARCH        │ {"query": "shoes"}

Query: "Get all events for user u001 ordered by time"
→ All u001 data is on ONE node (partition key = user_id)
→ Already sorted by event_time (clustering key)
→ Single partition read → extremely fast

CQL (Cassandra Query Language):
CREATE TABLE user_events (
    user_id    TEXT,
    event_time TIMESTAMP,
    event_type TEXT,
    metadata   TEXT,
    PRIMARY KEY (user_id, event_time)
) WITH CLUSTERING ORDER BY (event_time DESC);

SELECT * FROM user_events
WHERE user_id = 'u001'
  AND event_time > '2024-01-15 00:00:00';

Cassandra's Design Principles

1. Write-optimised
   Writes go to memtable (in memory) + commit log → returns immediately
   Background: flushes to SSTables on disk (sequential writes = fast)
   No read-before-write → extremely high write throughput

2. Masterless (peer-to-peer)
   No single master → no single point of failure
   Any node can serve any request
   Data replicated across N nodes (replication factor)

3. Tunable consistency
   On each read/write, choose consistency level:
   ONE:    fastest, least consistent (read/write one node)
   QUORUM: balance (read/write majority of replicas)
   ALL:    slowest, most consistent (all replicas must respond)

4. Partition key design is CRITICAL
   Bad partition key → hotspot → one node overloaded
   Good partition key → even distribution → all nodes share load

   ❌ Bad: partition key = user_country (millions of users in 'IN' → one hot partition)
   ✅ Good: partition key = user_id (evenly distributed)
   ✅ Good: partition key = (user_id, month) (for time-series, prevents unbounded growth)

Use Cases

1. Time-series data (best fit)
   IoT sensor readings, application metrics, user activity logs
   Query pattern: "get all readings for device X in last 24 hours"
   Partition by device_id, cluster by timestamp → perfect fit

   InfluxDB, TimescaleDB are also options for pure time-series.

2. User activity feeds / timelines
   "Get last 100 actions for user alice"
   Partition by user_id, cluster by timestamp
   Cassandra serves this in one partition read

3. Messaging systems (at scale)
   WhatsApp-like: messages partitioned by conversation_id
   "Get last 50 messages in conversation X" → single partition

4. High-write-throughput counters
   Ad impression tracking: millions of writes/second
   Cassandra's write path (memtable → SSTable) handles this
   Counter tables: COUNTER data type, distributed increment

5. Recommendation engine data
   "Users who bought X also bought Y" → pre-computed, stored in Cassandra
   Served at low latency on product pages

Limitations

- Read patterns must be designed upfront (query-first data modeling)
- No ad-hoc queries (every query must hit the partition key)
- No JOINs
- Eventually consistent by default (tunable, but strong consistency costs throughput)
- Deletes are expensive (tombstones — deletes are written, not actually deleted)
- Schema changes require careful migration (not schema-free like MongoDB)

4. Graph Databases

What It Is

Stores data as nodes (entities) and edges (relationships), both with properties. Designed for data where relationships are first-class citizens — traversing connections is the primary operation.

Relational DB to find "friends of friends":
  SELECT u2.name
  FROM users u1
  JOIN friendships f1 ON u1.id = f1.user_id
  JOIN friendships f2 ON f1.friend_id = f2.user_id
  JOIN users u2 ON f2.friend_id = u2.id
  WHERE u1.id = 1 AND u2.id != 1;
  → Multiple JOINs → O(n²) → catastrophic at scale (6 degrees of separation = 6 JOINs)

Graph DB:
  MATCH (alice:User {id:1})-[:FRIENDS_WITH*2]->(fof:User)
  WHERE fof.id != 1
  RETURN fof.name
  → Graph traversal → O(k^d) where k=avg connections, d=depth → efficient

Graph Model

Nodes:  entities (User, Product, Page, Company)
Edges:  relationships between nodes (FRIENDS_WITH, PURCHASED, WORKS_AT)
Properties: both nodes and edges can have properties

Example — social network:
(Alice: User {age:28, city:"Mumbai"})
     │
     ├─[FRIENDS_WITH {since: 2020}]──→ (Bob: User {age:34})
     │
     ├─[PURCHASED {date: "2024-01-10", amount: 500}]──→ (Shoes: Product)
     │
     └─[WORKS_AT {since: 2022, role: "Engineer"}]──→ (TechCorp: Company)

(Bob: User)
     └─[FRIENDS_WITH]──→ (Carol: User)

Neo4j — The Primary Example (Cypher Query Language)

// Create nodes
CREATE (alice:User {id: 1, name: "Alice", age: 28})
CREATE (bob:User {id: 2, name: "Bob", age: 34})
CREATE (shoes:Product {id: "P01", name: "Running Shoes", price: 2999})

// Create relationships
MATCH (alice:User {id: 1}), (bob:User {id: 2})
CREATE (alice)-[:FRIENDS_WITH {since: 2020}]->(bob)

MATCH (alice:User {id: 1}), (shoes:Product {id: "P01"})
CREATE (alice)-[:PURCHASED {date: "2024-01-10"}]->(shoes)

// Find friends of Alice
MATCH (alice:User {id: 1})-[:FRIENDS_WITH]->(friend:User)
RETURN friend.name

// Friends of friends (2 hops)
MATCH (alice:User {id: 1})-[:FRIENDS_WITH*2]->(fof:User)
WHERE fof.id <> 1
RETURN DISTINCT fof.name

// Recommendation: products bought by alice's friends that alice hasn't bought
MATCH (alice:User {id: 1})-[:FRIENDS_WITH]->(friend:User)
      -[:PURCHASED]->(product:Product)
WHERE NOT (alice)-[:PURCHASED]->(product)
RETURN product.name, COUNT(friend) AS friend_count
ORDER BY friend_count DESC
LIMIT 5

Use Cases

1. Social networks
   LinkedIn: 2nd-degree connections ("People you may know")
   Facebook: friend recommendations
   Twitter: follower graph, "Who to follow"
   → Graph traversal is the core operation

2. Fraud detection
   "Find accounts that share device_id, IP, or phone number"
   "Detect ring fraud: accounts transferring money in a circle"
   → Graph patterns (cycles, clusters) → efficient in graph DB
   → PayPal, Stripe use graph-based fraud detection

3. Recommendation engines
   "Users who bought X also bought Y" → product graph
   "Movies similar to what you watched" → content similarity graph
   → Netflix, Amazon product graph

4. Knowledge graphs
   Google Knowledge Graph: entities and their relationships
   "Who directed Inception?" → Person node, Movie node, DIRECTED edge

5. Access control / permission systems
   Role → Permission → Resource hierarchy
   "Does this user have access to this resource via any role?"
   → Graph traversal replaces complex RBAC SQL queries

6. Supply chain
   Supplier → Manufacturer → Distributor → Retailer → Customer
   "If this supplier fails, what products are affected?"
   → Impact analysis via graph traversal

Limitations

- Not for general-purpose data storage (poor at non-relationship queries)
- Not designed for high write throughput (maintaining graph edges is expensive)
- Scaling horizontally is hard (graph data is highly connected, hard to partition)
- Limited transaction support compared to RDBMS
- Niche use case — most applications don't need a graph DB
- Small ecosystem compared to relational and document DBs

Choosing the Right NoSQL Type — Decision Framework

What is your PRIMARY access pattern?

"Look up a value by a single key instantly"
→ Key-Value (Redis, DynamoDB)
  Examples: sessions, cache, rate limiting, distributed locks

"Store and query flexible, nested, document-like data"
→ Document (MongoDB, Firestore, Couchbase)
  Examples: product catalog, user profiles, CMS content

"Write massive amounts of time-series or event data,
 query by a known partition + time range"
→ Column-family (Cassandra, HBase)
  Examples: activity logs, IoT, metrics, messaging at scale

"Traverse relationships between entities efficiently"
→ Graph (Neo4j, Amazon Neptune)
  Examples: social networks, fraud detection, recommendations

Side-by-Side Comparison

DimensionKey-Value (Redis)Document (MongoDB)Column-family (Cassandra)Graph (Neo4j)
Data modelkey → blobkey → JSON documentpartition key → rowsnodes + edges
Query flexibilityKey onlyRich queriesPartition key requiredGraph traversal
Write speedExtremely fastFastExtremely fastModerate
Read speedO(1) by keyFast with indexesFast within partitionFast for traversal
Horizontal scaleEasyGood (sharding)Excellent (masterless)Hard
ConsistencyStrong (single node)ConfigurableTunable (eventual default)Strong
RelationshipsNoneManual referenceNoneFirst-class
SchemaNoneFlexibleSemi-rigid (partition key)Flexible
Best forCache, sessionsCatalogs, profilesEvents, time-seriesSocial, fraud

System Design Context — Polyglot Persistence

Real systems use MULTIPLE databases together, each for what it does best.

Flipkart-like e-commerce architecture:

┌──────────────────────────────────────────────────────────────┐
│ MySQL (Relational)                                           │
│   orders, payments, users, invoices                          │
│   → ACID required, complex joins, financial data            │
├──────────────────────────────────────────────────────────────┤
│ MongoDB (Document)                                           │
│   product catalog, reviews, user preferences                 │
│   → Flexible schema, varying product attributes             │
├──────────────────────────────────────────────────────────────┤
│ Redis (Key-Value)                                            │
│   sessions, cart contents, inventory cache, rate limiting    │
│   → Millisecond latency, temporary data, counters           │
├──────────────────────────────────────────────────────────────┤
│ Cassandra (Column-family)                                    │
│   user activity events, clickstream, notifications history  │
│   → Write-heavy, time-series, massive scale                 │
├──────────────────────────────────────────────────────────────┤
│ Elasticsearch (Inverted Index — special type)               │
│   product search, full-text search, log analysis             │
│   → Full-text search, faceted filtering, relevance ranking  │
└──────────────────────────────────────────────────────────────┘

Each DB is used where it excels.
This pattern is called Polyglot Persistence.

How This Connects

  • Relational Model - Tables, Keys, Relationships — the baseline NoSQL trades away: foreign keys, JOINs, a fixed schema. Know what you're giving up.
  • ACID Properties / CAP Theorem / PACELC Theorem — most NoSQL stores lean toward BASE / AP / tunable consistency; that trade is the reason they scale writes.
  • Normalization - 1NF to BCNF — document stores deliberately denormalize (embed related data) so a read is one lookup instead of a join.
  • Consistent Hashing / Sharding & Partitioning Strategies — the partition/shard key in Cassandra and DynamoDB is exactly this; a bad key = a hot node.
  • Redis as a Caching Engine — the key-value family in depth: data structures, TTLs, INCR, distributed locks.
  • Module 4 — Data Architecture — "SQL vs NoSQL tradeoffs", replication, and polyglot persistence are the full-length versions of this note's decision framework.

In the Interview

When to say it. Any "which database?" question, any time your design has a component with a clear single access pattern (sessions, a feed, a catalog, a social graph), and any scaling follow-up where the relational choice hits a wall.

The ~20-second version:

"I choose by the primary access pattern. Sub-millisecond lookups by a single key — sessions, cache, rate-limit counters, distributed locks — go to a key-value store like Redis. Data with varying, nested attributes — a catalog where a shirt has sizes and a laptop has specs — fits a document store like MongoDB without a rigid schema. Massive write throughput of time-ordered events — activity logs, IoT, clickstream — suits a column-family store like Cassandra, whose masterless write path and partition-key model handle it; the partition key is the critical design choice because a bad one creates a hot node. If the core operation is traversing relationships — friends-of-friends, fraud rings, permission chains — a graph database like Neo4j does multi-hop queries that would be catastrophic joins in SQL. In practice it's polyglot: MySQL for the money and orders, Redis for sessions, Mongo for the catalog, Cassandra for events."

Follow-ups to expect:

  1. "Cassandra is 'column-family' — is that a columnar/analytics DB like Redshift?" — No. It's row-oriented storage with a partition key + clustering columns and flexible columns per row; it's built for known-access-pattern OLTP-style writes/reads at scale, not ad-hoc analytical scans.
  2. "What's the cost of MongoDB's schema flexibility?" — Validation moves to the application, there are no cross-collection JOINs (you denormalize or reference-and-fetch), and multi-document transactions exist but carry a performance cost.
  3. "Why is the partition key so important in Cassandra/DynamoDB?" — It decides which node stores the data. Low-cardinality or skewed keys (e.g. country) create hot partitions; you want high cardinality and even access (e.g. user_id, or (user_id, month) to bound partition growth).
  4. "When is a graph DB the wrong choice?" — For general storage or high write throughput — maintaining edges is expensive and graphs are hard to shard. Most apps don't need one.
  5. "Where does Elasticsearch fit?" — It's a search-specific store built on an inverted index (term → document ids), synced from the source of truth via CDC — used for full-text and faceted search, not as a primary datastore.

Mistake to avoid: "NoSQL because it's faster / more scalable" with no access pattern named. Also don't call the relational option obsolete — for transactional, relationship-rich, correctness-critical data it's still the default, and NoSQL sits alongside it.


Quick Revision — 7 Lines

  1. Key-Value (Redis) → O(1) by key. Sessions, cache, rate limiting, distributed locks.
  2. Document (MongoDB) → flexible JSON. Product catalog, user profiles, CMS. Rich queries.
  3. Column-family (Cassandra) → partition key + clustering. Write-heavy time-series. Masterless.
  4. Graph (Neo4j) → nodes + edges. Social graphs, fraud detection. Multi-hop traversal.
  5. Cassandra partition key design is critical — bad key = hotspot = one overloaded node.
  6. NoSQL generally sacrifices JOINs and ACID for scale, speed, and schema flexibility.
  7. Real systems use polyglot persistence — multiple databases, each for its strength.

Quick-revision quiz

Question 1 of 17
Name the four NoSQL families, one canonical example each, and the primary access pattern each is built for.