NoSQL Types (intro)
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
| Dimension | Key-Value (Redis) | Document (MongoDB) | Column-family (Cassandra) | Graph (Neo4j) |
|---|---|---|---|---|
| Data model | key → blob | key → JSON document | partition key → rows | nodes + edges |
| Query flexibility | Key only | Rich queries | Partition key required | Graph traversal |
| Write speed | Extremely fast | Fast | Extremely fast | Moderate |
| Read speed | O(1) by key | Fast with indexes | Fast within partition | Fast for traversal |
| Horizontal scale | Easy | Good (sharding) | Excellent (masterless) | Hard |
| Consistency | Strong (single node) | Configurable | Tunable (eventual default) | Strong |
| Relationships | None | Manual reference | None | First-class |
| Schema | None | Flexible | Semi-rigid (partition key) | Flexible |
| Best for | Cache, sessions | Catalogs, profiles | Events, time-series | Social, 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:
- "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.
- "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.
- "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). - "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.
- "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
- Key-Value (Redis) → O(1) by key. Sessions, cache, rate limiting, distributed locks.
- Document (MongoDB) → flexible JSON. Product catalog, user profiles, CMS. Rich queries.
- Column-family (Cassandra) → partition key + clustering. Write-heavy time-series. Masterless.
- Graph (Neo4j) → nodes + edges. Social graphs, fraud detection. Multi-hop traversal.
- Cassandra partition key design is critical — bad key = hotspot = one overloaded node.
- NoSQL generally sacrifices JOINs and ACID for scale, speed, and schema flexibility.
- Real systems use polyglot persistence — multiple databases, each for its strength.