NoSQL
ACID vs BASE
A user clicks "Pay Now" on a checkout page. Their browser is slow — they click again. Two identical payment requests hit your server at the same millisecond. Your database processes both. The customer is charged £340 twice. Their bank account is short £340. Your support queue explodes. This is not a theoretical problem — it happens to real companies every week. The difference between a system where this is impossible and one where it happens daily comes down to one choice: ACID or BASE.
ACID — Four Guarantees, Each One Critical
ACID is a set of four properties that a database transaction must have to be considered reliable. Each letter protects you from a specific class of failure. Lose any one of them and data corruption becomes possible.
Atomicity — All or Nothing
A transaction is a single unit. Either every operation inside it succeeds — or none of them do. There is no "half done."
Consistency — Rules Always Hold
A transaction can only bring the database from one valid state to another valid state. It can never break your defined rules.
Isolation — Transactions Don't See Each Other
Concurrent transactions execute as if they were running one at a time. A transaction in progress is invisible to all other transactions.
Durability — Committed Means Saved
Once a transaction is committed, it stays committed. A power cut, a server crash, a kernel panic — none of it can undo a committed write.
ACID in Code — A Real Bank Transfer
The scenario: You're building the transfer feature for a fintech app. User A sends £200 to User B. This requires two database writes — debit A, credit B. Here's how ACID protects this in PostgreSQL:
-- Start the transaction — nothing is saved until COMMIT
BEGIN;
-- Step 1: Debit the sender
UPDATE accounts
SET balance = balance - 200
WHERE user_id = 'user_A'
AND balance >= 200; -- consistency check: can't go negative
What each line does:
BEGIN;
Opens a transaction. From this point, all changes are held in a temporary state — visible only to this connection, not yet permanent. If anything fails, everything since BEGIN is discarded.
AND balance >= 200
This is a consistency guard — the UPDATE only runs if the sender has enough money. If balance is £50 and they try to send £200, zero rows are updated. The transaction is then rolled back. No overdraft possible.
-- Step 2: Check that Step 1 actually updated a row
-- If 0 rows were updated, the sender didn't have enough balance
-- ROLLBACK undoes everything — atomicity in action
GET DIAGNOSTICS row_count = ROW_COUNT;
IF row_count = 0 THEN
ROLLBACK;
RAISE EXCEPTION 'Insufficient balance';
END IF;
-- Step 3: Credit the receiver
UPDATE accounts
SET balance = balance + 200
WHERE user_id = 'user_B';
ROLLBACK;
Atomicity in action. If Step 1 found insufficient balance, ROLLBACK cancels the entire transaction. The debit never happened. user_A's balance is unchanged. user_B gets nothing. The system is in exactly the state it was before.
UPDATE accounts SET balance = balance + 200
This write is still inside the transaction — still not permanent. It's isolated from every other connection reading the accounts table right now.
-- Step 4: Make it permanent — both writes saved atomically
COMMIT;
-- Before transfer: user_A balance: £500 user_B balance: £100 -- After COMMIT: user_A balance: £300 (-£200) user_B balance: £300 (+£200) -- Total money in system: £600 → £600 ✓ (no money created or destroyed)
COMMIT — all four ACID properties at once:
Atomicity: Both updates committed together — impossible for one to succeed without the other.
Consistency: Total balance in the system stayed at £600 — money was neither created nor destroyed.
Isolation: No other connection saw the in-between state where A was debited but B wasn't yet credited.
Durability: The committed balances survive any crash, power cut, or restart.
What Happens Without ACID — The Race Condition
The scenario: Same transfer — but this time there's no transaction. Two checkout requests for the same item arrive at exactly the same millisecond. Watch what breaks:
❌ Without transaction (broken)
-- Request 1 reads stock: 1 item left
SELECT stock FROM products
WHERE id = 'ITEM_99';
-- Returns: 1
-- Request 2 ALSO reads stock: 1 item left
SELECT stock FROM products
WHERE id = 'ITEM_99';
-- Returns: 1
-- Both think 1 item is available
-- Both proceed to place the order
-- Both decrement stock
UPDATE products SET stock = stock - 1
WHERE id = 'ITEM_99';
-- stock is now: -1
✅ With transaction (fixed)
BEGIN;
-- SELECT FOR UPDATE locks this row
-- Request 2 must WAIT until Request 1 finishes
SELECT stock FROM products
WHERE id = 'ITEM_99'
FOR UPDATE;
-- Returns: 1 (locked)
UPDATE products
SET stock = stock - 1
WHERE id = 'ITEM_99'
AND stock > 0;
COMMIT;
-- Lock released. Request 2 now runs.
-- Sees stock = 0. Zero rows updated.
-- ROLLBACK. "Out of stock" returned.
The race condition explained:
Both read stock = 1 simultaneously
This is called a dirty read. Without isolation, two transactions read the same row at the same time — both see 1, both think they can proceed.
stock ends up at -1
Two orders placed for one item. One customer will receive it. The other will be left waiting indefinitely while your warehouse team scratches their heads. This exact bug caused a famous double-booking incident at a major airline in 2019.
SELECT FOR UPDATE
This is PostgreSQL's row-level lock. The first transaction to run this line owns the lock. Request 2 hits this line and waits — it cannot read or modify this row until Request 1 commits or rolls back. Isolation enforced.
BASE — The Alternative Contract
BASE is not a weakness — it's a deliberate design choice made by NoSQL databases that prioritise scale and availability over strict consistency. It stands for three things:
Basically Available
The system guarantees availability — it always responds. But the response might be a slightly stale value, or a partial result. It never just goes dark. Think of it as: "I'll always give you an answer. I just can't promise it's the very latest one."
Soft State
The state of the system may change over time — even without new input. Data is being replicated, synced, and reconciled in the background. The system is always moving toward a consistent state, even if it's not there yet.
Eventually Consistent
Given enough time with no new writes, all nodes will converge to the same value. The guarantee is not right now — it's eventually. For most use cases, "eventually" means milliseconds to seconds.
BASE in Code — The Same Transfer, Different World
The scenario: You're tracking user wallet balances in Cassandra for a gaming platform. Points are awarded constantly — thousands per second. Perfect consistency isn't required — if a player sees 4,820 points instead of 4,821 for half a second, nothing breaks. But the system must never slow down or refuse a write.
from cassandra.cluster import Cluster
from cassandra import ConsistencyLevel
from cassandra.query import SimpleStatement
session = Cluster(['node1','node2','node3']).connect('gaming')
Cluster(['node1','node2','node3']) — connects to a 3-node Cassandra cluster. All three hold copies of the data. The driver picks the fastest node for each request.
# Add 50 points to a player's balance
# ONE = only one node needs to confirm — fastest possible write
add_points = SimpleStatement(
"UPDATE wallets SET points = points + 50 WHERE player_id = %s",
consistency_level=ConsistencyLevel.ONE
)
session.execute(add_points, ('player_7721',))
-- Write acknowledged immediately by node1 -- node2 and node3 will sync within ~50ms Result: Applied Latency: 1.2ms -- If player reads balance right now from node2: -- Might see: 4,820 (old value — node2 not synced yet) -- 50ms later from node2: -- Will see: 4,870 (synced — eventually consistent)
BASE behaviour explained:
ConsistencyLevel.ONE
This is BASE's "Basically Available" in action. Write confirmed the moment one node saves it. No waiting for the other two. 1.2ms response time instead of the 8–15ms needed to confirm on multiple nodes.
Might see 4,820 then 4,870
This is "Soft State" + "Eventually Consistent." The value is in transition — node2 hasn't received the sync yet. For 50ms, different nodes give different answers. Then they converge. No manual intervention. No rollback. Just time.
No BEGIN / COMMIT / ROLLBACK
Cassandra has no concept of multi-row transactions in the traditional sense. Each write is independent. This is why it can handle a million writes per second — there's no coordination overhead, no lock manager, no transaction log to coordinate across nodes.
The ACID vs BASE Spectrum
It's not a binary switch — it's a dial. Different databases and different settings land at different points on this spectrum:
MySQL
Full ACID
Strong consistency
(w:majority)
Near-ACID
Tunable
DynamoDB
Configurable
Per-operation
(QUORUM)
Leaning BASE
Configurable
CouchDB (ONE)
Full BASE
Max availability
Notice: Many databases let you choose where you land on this spectrum per-query. MongoDB with w:1 leans BASE. With w:majority it leans ACID. You're not locked in — but you have to choose consciously.
When Each Model Wins — 4 Real Scenarios
| Scenario | Use | Why |
|---|---|---|
| Bank transfer — debit A, credit B | ACID | Partial failure = money disappears. Atomicity is non-negotiable. £1 lost is a regulatory incident. |
| Social media like count updates | BASE | Showing 4,201 vs 4,202 likes for 100ms hurts nobody. Locking every like write at Instagram scale would melt the database. |
| E-commerce inventory — last item in stock | ACID | Two users buying the same last item simultaneously. Without isolation, both orders succeed and stock goes negative. Overselling is a real business crisis. |
| IoT sensor readings — 100k devices/sec | BASE | A temperature reading 200ms stale is irrelevant. Running ACID transactions across 100,000 concurrent writes per second is physically impossible at reasonable cost. |
The Consequences of Choosing Wrong
Using BASE where you need ACID
Using ACID where you need BASE
Teacher's Note
The most dangerous engineer is the one who learned ACID in university and applies it to everything, or the one who heard "NoSQL is faster" and removed all transaction logic from their payment system. Both are expensive mistakes. The skill is knowing which guarantee each piece of your system actually needs — and matching the database model to that requirement deliberately.
Practice Questions — Spot the Bug
Scenario:
UPDATE seats SET status='booked' WHERE seat_id='12A';INSERT INTO bookings (user_id, seat_id) VALUES ('u_99', '12A');The server crashes between the two statements. The seat is marked as booked but no booking record exists. The seat is now permanently reserved for nobody. Which ACID property was violated?
Scenario:
Scenario:
Quiz — Production Incident. ACID or BASE Failure?
Incident report:
Incident report:
Architecture decision:
Up Next · Lesson 7
Types of NoSQL Databases
A deep dive into all four NoSQL families — key-value, document, column-family, and graph — with real data, real code, and the exact moment each one becomes the obvious right choice.