Absolutely. I’ll keep the same approach as the previous notes: easy language, ASCII diagrams, interview-focused, and without unnecessary length. Based only on the uploaded material.
In very simple words: A NoSQL database is not simply “a faster database”. When you send a request, the system has to figure out where the data lives, which node owns it, which copies exist, and how fresh and durable the result needs to be. NoSQL is mainly about making these distributed choices work at scale.
Don't think:
App → Database → Put/Get
Think:
App ↓ Coordinator ↓ Which partition? ↓ Which node? ↓ Which replicas? ↓ Disk
A NoSQL cluster is mainly about:
WHERE does the data live? HOW many copies exist? HOW fresh is the data? HOW durable is the data?
Not automatically.
NoSQL gives different trade-offs:
NoSQL ├── Horizontal scaling ├── Flexible schema ├── High write throughput └── Often relaxed consistency
The reason to choose NoSQL should be:
A specific access pattern or scale requirement — not simply "NoSQL is faster."
In very simple words: The four families are useful because they organize data in different ways. Key-value is like looking up a complete object by its key; document stores related information together; wide-column is useful for very large ordered datasets; and graph databases are built for following relationships. The right choice depends on how the application asks for the data.
NoSQL
|
+--------------+--------------+
| | |
Key-Value Document Wide-Column
| | |
lookup by key related fields huge/time
ordered data
|
Graph
|
relationships
user:1001 → {entire object}
Best when:
I always read/write the entire object using its key.
user:1001
↓
{name, city, orders, ...}
Best when:
Data is semi-structured and related fields are read together.
customer_id = 1001 10:00 → transaction 10:05 → transaction 10:10 → transaction
Best for:
Huge datasets + time/range-based queries.
Account A
|
├── owns → Account B
|
└── linked → Customer C
|
└── owns → Account D
Best when:
Relationships and traversal are the query.
Before designing:
"What are my access patterns?"
↓
"At what scale?"
↓
"How should I partition the data?"
Schemaless ≠ No schema.
The schema moves into:
Application
+
Access patterns
+
Partition design
In very simple words: Partitioning decides which machine stores a piece of data. Range partitioning keeps nearby values together and helps range searches, while hash partitioning spreads values across machines and helps distribute load. A bad partition key can make one machine extremely busy even when many other machines are idle.
Partitioning answers:
WHERE does this record live?
User 1–10000 → Node A User 10001–20000 → Node B User 20001–30000 → Node C
Good for:
Range queries
But can create hotspots.
hash(customer_id) % N
↓
Partition
Example:
Customer A → Node 2 Customer B → Node 7 Customer C → Node 1 Customer D → Node 5
Good for:
Even distribution
But range queries become harder.
Suppose an IPO has:
scheme_id = ABC123
Thousands of users hit it simultaneously:
ABC123 ↓ Partition X ↓ 🔥🔥🔥🔥🔥🔥 HOT PARTITION
Adding nodes may not solve this if the hot key itself remains on one partition.
RANGE
Adjacent values stay together
↓
Great range scans
↓
Possible hotspots
HASH
Values spread out
↓
Great load distribution
↓
Range scans become scatter-gather
Use a composite design:
customer_id
↓
HASH → chooses partition
transaction_time
↓
SORT/RANGE within partition
Result:
Customer A partition ├── Jan 1 ├── Jan 2 ├── Jan 3 └── Jan 4
In very simple words: Partitioning and replication solve different problems. Partitioning decides where one copy of the data lives across machines; replication decides how many copies exist so the system can survive failures. You normally need to think about both separately.
Very important distinction:
PARTITIONING
↓
Where does the data live?
REPLICATION
↓
How does the data survive failure?
They solve different problems.
In very simple words: Replication means keeping copies of data on multiple nodes. In a leader-based design, one node normally handles writes and copies the changes to replicas, which makes consistency simpler but creates a failover concern if the leader dies. In a leaderless design, multiple nodes can participate and correctness is controlled using quorum rules.
Leader
A
/ \
↓ ↓
B C
Replica Replica
Writes normally go through the leader.
One authoritative writer
↓
Simpler consistency
Leader dies
↓
Failover
↓
Temporary interruption
Partition P Node A ← write Node B ← write Node C ← write All can participate
Correctness is based on quorum.
W = Write quorum R = Read quorum N = Number of replicas
In very simple words: Having three copies does not automatically mean a write is safe. If the client gets an acknowledgement after only one node has received the write and that node dies, the write may be lost. The important question is how many replicas must confirm the write before the system tells the client it succeeded, and where those replicas are physically placed.
Suppose:
N = 3
You have three copies.
But:
Write ↓ Node A ACK ↓ Client gets success ↓ B and C haven't received it
If A crashes:
Write LOST
So:
Durability depends on how many replicas must acknowledge the write, not merely how many copies eventually exist.
Don't put replicas too close physically:
BAD Rack 1 A B C
A rack failure can kill all three.
Better:
AZ 1 → A AZ 2 → B AZ 3 → C
Why?
Failures are often correlated.
In very simple words: A write normally travels through a coordinator and storage node, gets recorded in a durable log, is placed in memory, and is sent to replicas before the required acknowledgement is returned. The WAL or commit log is important because it gives the system something to use for recovery after a crash. Waiting for more replica confirmations gives stronger safety but adds latency.
Typical flow:
Client ↓ Coordinator ↓ Leader | +──→ WAL / Commit Log | +──→ Memtable | +──→ Replicas | ↓ ACK
WAL = recovery source of truth
If:
Memory updated
↓
Process crashes
↓
WAL wasn't written
The update cannot be recovered.
Therefore:
WAL ↓ Memtable ↓ Replication
Fast but risky:
Memtable updated
↓
ACK
↓
Replicate later
Leader crashes:
LOST WRITE
Safer:
Write ↓ Enough replicas confirm ↓ ACK
More latency, but stronger durability.
In very simple words: A write can reach the main node before all replicas have caught up. If the next read goes to a slower replica, the user may briefly see the old value. That delay between the latest write and a replica catching up is called replication lag.
Example:
Customer redeems MF
↓
Write accepted
↓
Refresh after 200 ms
↓
Old balance appears
Why?
Write → Leader
↓
Replicas
↓
propagation delay
The read may hit:
Replica that hasn't caught up yet
This is replication lag.
In very simple words: Read-your-writes means that after a user changes something, their next read should show that change. A simple approach is to route that user’s reads to the leader or to a replica known to be caught up. The cost is that some reads cannot be spread as freely across all replicas.
Means:
After I write something, I should immediately see my own change.
Example:
Redeem MF ↓ Refresh ↓ Must see updated balance
Possible solution:
After write
↓
Route user's reads
↓
Leader / caught-up replica
Trade-off:
More consistency
↓
Less read scalability
In very simple words: Quorum is a way to make reads and writes overlap. If there are N copies, and the write needs W confirmations while the read checks R copies, W + R greater than N guarantees that at least one copy belongs to both groups. That overlap helps the read find the latest value.
Suppose:
N = 3 W = 2 R = 2
Then:
W + R = 4 4 > 3
Therefore read and write sets must overlap.
WRITE
[A][B]
READ
[B][C]
↑
overlap
So the read can find the latest version.
N = 3 W = 1 R = 1 1 + 1 = 2 2 <= 3
No overlap guarantee.
WRITE → A READ → C A ≠ C
Potential stale read.
In very simple words: Strong consistency waits for the required coordination before returning success, so the result is fresher but may be slower. Eventual consistency returns quickly and lets other copies catch up afterward. In production, the notes emphasize measuring the actual replication lag instead of simply saying “it will eventually be consistent.”
Write ↓ Wait for required replicas ↓ Confirm ↓ Return
More:
Consistency ↑ Latency ↑
Write ↓ Return quickly ↓ Replicas catch up later
More:
Speed ↑ Freshness ↓ temporarily
Don't just say:
"Eventually it will be consistent."
Measure:
Replication lag p50 = typical p99 = worst important case
This gives you a real staleness window.
In very simple words: In NoSQL, you usually decide how people will query the data before deciding exactly how to store it. The query determines the partition key and therefore where the data lives. For example, if you constantly ask for one customer’s newest orders, putting customer_id in the partition and time in the sort order makes that read simple.
This is one of the most important NoSQL concepts.
SQL:
Create table
↓
Later
↓
CREATE INDEX
↓
New query
NoSQL:
Query pattern
↓
Partition key
↓
Data placement
↓
Performance
Therefore:
Design your access patterns before designing the table.
Example:
Requirement:
Get all orders for customer 1001 newest first
Good:
Partition = customer_id Sort = created_at DESC
Bad:
Partition = order_id
In very simple words: A relational design can spread related information across several tables because SQL joins it when needed. Copying that structure directly into a distributed NoSQL system can create many network calls and scatter-gather work. The notes therefore recommend designing around the actual query and denormalizing when that makes the read path simpler.
SQL might have:
Customers
|
+--- Orders
|
+--- Products
If you copy this structure blindly into NoSQL:
Query ↓ Customer partition ↓ Order partition ↓ Product partition ↓ Network calls
Potentially:
Scatter + Gather + Multiple lookups
Instead:
Design around query
↓
Denormalize where needed
↓
Keep read path simple
In very simple words: A secondary index gives you another way to find the same data, but in a distributed database it may live somewhere different from the main record. One logical update can therefore require multiple updates, and the index can temporarily lag behind the primary data. Local indexes keep things closer together; global indexes support broader searches but involve more distributed work.
Primary data:
user_id → customer document
But now you want:
Find customer by email
Secondary index:
email → user_id
Now one logical write becomes:
Write customer
|
+----→ Primary storage
|
+----→ Secondary index
Potential problem:
Two partitions
↓
Two writes
↓
No natural atomicity
↓
Index can temporarily lag
So secondary indexes can introduce consistency challenges.
Same partition
↓
Base data + index
↓
Cheap consistency
But usually requires knowing the partition key.
Base data ↓ Partition A Index ↓ Different partition
Supports new query patterns, but:
More distributed work + Potential async lag
In very simple words: An LSM tree is designed to make writing data cheap. Instead of constantly changing random disk pages, the system records the write, keeps it in memory, flushes it into sorted files, and later merges those files during compaction. This makes it especially attractive for write-heavy workloads.
LSM = Log-Structured Merge Tree
Typical write path:
Write ↓ WAL ↓ Memtable ↓ Flush ↓ SSTable ↓ Compaction ↓ Merged SSTables
Writes are mostly:
Sequential ↓ Append ↓ Fast
Instead of constantly modifying disk pages in place.
In very simple words: LSM gives high write throughput, but the cost appears during reads and compaction. A value may temporarily exist in several files, so a read may need to check multiple places. If writes arrive faster than compaction can keep up, the number of files grows and important read latency, especially p99, can increase.
You gain:
High write throughput
But pay with:
Read amplification + Compaction cost
A key may exist in multiple SSTables:
Read key X SSTable 1 → check SSTable 2 → check SSTable 3 → check SSTable 4 → check
Compaction eventually merges them.
During a huge write workload:
Writes ↓ SSTables created quickly ↓ Compaction can't keep up ↓ More SSTables ↓ Reads check more files ↓ p99 latency ↑
So:
LSM is excellent for write-heavy workloads, but compaction must be managed.
In very simple words: Clients may remember which node owns each partition. When a new node is added or ownership changes, that information can become stale, causing requests to go to the wrong machine. The client therefore needs a way to detect the new partition map and refresh its routing information.
Clients may cache:
Partition → Node
Example:
Before: P1 → A P2 → B P3 → C
Add Node D:
After: P1 → A P2 → D P3 → C
If the client still thinks:
P2 → B
it sends requests to the wrong node.
Therefore:
Partition map
↓
Must detect changes
↓
Refresh
In very simple words: Rebalancing means moving real data from one machine to another while the system is still serving users. That movement consumes network and disk resources and must deal with reads, writes, and changing ownership at the same time. So adding capacity can itself create temporary production pressure.
Rebalancing means moving live data:
Node A | | move data ↓ Node D
while:
Reads continue Writes continue
Problems:
Data movement + Network usage + Disk usage + Ownership change + Concurrent writes
So rebalancing itself can affect production performance.
In very simple words: Split-brain happens when a network problem makes two parts of the cluster believe they are both the leader. Both sides can then accept different writes, creating two conflicting histories. This is especially dangerous for money and balances because the system needs one authoritative history.
One of the most dangerous failures.
Network partition
/ \
/ \
Node A Node B
↓ ↓
"I am leader" "I am leader"
↓ ↓
WRITE WRITE
Both sides accept different writes.
When network heals:
Node A → History A
Node B → History B
↓
CONFLICT
This is split-brain.
Especially dangerous for:
Money Balances Ledger
because there must be one authoritative history.
In very simple words: Replicas can become different because updates may arrive asynchronously. Instead of comparing every record, the system can compare compact hash or Merkle-like summaries to find which area differs. It then repairs only the mismatching range, which is much cheaper than comparing everything.
Replicas can drift because replication is often asynchronous.
Instead of comparing millions of rows:
Replica A vs Replica B vs Replica C
Use hash/Merkle-like summaries:
Entire dataset
↓
Hash tree
/ \
Hash Hash
/ \ / \
... ... ... ...
If hashes differ:
Find differing range
↓
Compare only that range
↓
Repair
Much cheaper than comparing everything.
In very simple words: Updating one record in one partition is relatively simple. Updating related data that lives in two different partitions becomes a distributed transaction, because more than one machine must agree on the change. That can require distributed-transaction mechanisms and adds complexity and cost.
Single record:
Update document
↓
Easy atomic operation
Two different partitions:
Account balance
+
Transaction log
↓
Different partitions
↓
Distributed transaction
Now you may need:
2PC / database-native distributed transaction
This is more expensive and complex.
In very simple words: When a record reaches its TTL, the system can consider it expired immediately for logical purposes even though the physical deletion happens later. A background process eventually removes it. This difference matters when the business or compliance requirement cares about exactly when data must no longer exist.
Suppose:
TTL = 24 hours
After 24 hours:
LOGICALLY EXPIRED
But physical deletion may happen later:
24h ↓ No longer logically valid ↓ Background cleanup ↓ Physical deletion
So:
Expired does not always mean physically deleted immediately.
Important for compliance requirements.
In very simple words: If two regions can write the same key at the same time, there may be no single leader deciding which update happened first. The system can therefore end up with conflicting versions. A multi-writer design needs an explicit conflict-resolution strategy.
Single region:
Region A ↓ Leader
Multi-writer:
Region A → WRITE X
Region B → WRITE Y
↓
Same key
↓
CONFLICT
Because there is no single leader deciding the order.
Therefore multi-writer systems need:
Conflict resolution strategy
In very simple words: Not every piece of data needs the same level of safety. The example deliberately uses stronger settings for important data such as a ledger and faster, weaker settings for a cache. The simple idea is to choose consistency and performance based on the value and risk of the data.
A NoSQL platform might deliberately use different consistency levels:
NoSQL Cluster
|
+-----------+-----------+
| |
Ledger Cache
| |
W=2, R=2 W=1, R=1
| |
Stronger safety Faster
This is not inconsistent engineering.
It is:
Consistency level chosen per table based on the value of the data.
In very simple words: The examples show how theoretical design choices become real incidents: a popular IPO can create a hot partition, a lagging replica can show an old portfolio value, a secondary index can fall behind, heavy writes can overload compaction, and weak read/write settings can produce stale data. The fix is to design partitioning, routing, consistency, and write pressure around the actual workload.
Popular IPO
↓
Many applicants
↓
Same partition
↓
HOT
Fix:
Better bucket / virtual-node distribution
Redeem ↓ Leader updated ↓ Read hits lagging replica ↓ Old balance
Fix:
Read-your-writes routing
Bulk update ↓ Primary updated ↓ Secondary index behind
Fix:
Rate-limit index updates + Define staleness SLO
EOD settlement ↓ Huge writes ↓ SSTables ↑ ↓ Compaction overloaded ↓ p99 latency ↑
Fix:
Rate-limit settlement writes
W=1/R=1 ↓ Fast but stale ↓ Wrong valuation
Fix:
Critical lien reads → W=2/R=2 Display-only widget → W=1/R=1
In very simple words: The NoSQL design flow is: first understand the query, then choose where the data should live, then decide how many copies you need, when a write is safe, how fresh reads must be, and which storage/indexing approach fits. The key lesson is that NoSQL structure follows access patterns rather than being designed first and queried later.
Remember NoSQL design as this flow:
ACCESS PATTERN
↓
What do I query?
↓
PARTITION KEY
↓
Where does data live?
↓
REPLICATION
↓
How does it survive?
↓
WRITE / READ QUORUM
↓
How fresh/durable?
↓
STORAGE ENGINE
↓
B-Tree / LSM / etc.
↓
INDEX + DATA MODEL
↓
Does it scale safely?
1. What are my access patterns? 2. What is my partition key? 3. Can this key become HOT? 4. How many replicas do I need? 5. When do I ACK a write? 6. How stale can a read be? 7. What happens when a node/network/AZ dies?
NoSQL is not "SQL without joins." It is a deliberate trade-off around data placement, scalability, availability, consistency, and access patterns.
And the most important rule:
SQL: Data → Add index → Query NoSQL: Query → Design partition → Design data
Design the access pattern first. The database structure follows from it.