Understanding Database ACID Properties: A Tale of Tate and Zuck

AnyOneCanCode
10 min readDec 28, 2024

--

INTRODUCTION

Have you ever wondered what ACID properties are? Why do we need them? is this person talking about chemical acid? What does it have to do with databases? what is a transaction?

Transaction is: a sequence of database operations that satisfies ACID properties.

ACID is:
Atomicity: the transaction is a single unit, either completely successful or completely failed.
Consistency: ensure transaction must bring the database from one consistent state to another. data should always follow all the database-defined rules (constraints, cascades .. etc).
Isolation: concurrent transactions should have the same effect as if they were executed sequentially. Failed transactions should not affect others, as they are running in isolation.
Durability: once a transaction is committed, the database must preserve its data state even in cases of failure (server crashes, power outages, etc.) by recording the commit in non-volatile memory.

Let’s dig deeper into the details so you won’t say I’m just scratching the surface, and I don’t want to look dumb in this smart Zuckerberg world.

ATOMICITY

Ensured a series of DB operations under transaction should either be completely successful or completely failed. there should not be any partially successful operation.

Let’s understand this through an example involving two people: Zuck’s and Tate’s.
Zuck’s and Tate’s are good friends, and Tate’s decides to send money to Zuck’s to help him escape the matrix.

Case 1: Atomic Success

Before Transaction:
Tate’s balance: $1000
Zuck’s balance: $500

Tate’s send $100 to Zuck’s

After Transaction:
Tate’s balance: $900
Zuck’s balance: $600

CASE 1: ATOMIC SUCCESS 

START TRANSACTION
// Initial State Check
Tate's Balance = $1000
Zuck's Balance = $500

// Transaction Steps
Step 1: Check if Tate's has enough money
IF Tate's Balance >= $100 THEN continue
ELSE rollback

Step 2: Deduct from Tate's account
Tate's Balance = Tate's Balance - $100 // Now $900

Step 3: Add to Zuck's account
Zuck's Balance = Zuck's Balance + $100 // Now $600

// Everything worked!
COMMIT TRANSACTION

// Final State
Tate's Balance = $900 ✓
Zuck's Balance = $600 ✓
Transaction Complete ✓

This is an atomic success case where the first debit occurs from Tate’s account and then credit occurs in Zuck’s account. In the above code, all the DB operations are executed inside the transaction block, so if any operation fails, we can roll back the entire series of operations. We only commit the transaction after all the DB operations are successful.

Case 2: Atomic Failure

Before Transaction:
Tate’s balance: $1000
Zuck’s balance: $500

Tate’s failed to send $100 to Zuck’s

After Transaction:
Tate’s balance: $1000
Zuck’s balance: $500

CASE 2: ATOMIC FAILURE

START TRANSACTION
// Initial State Check
Tate's Balance = $1000
Zuck's Balance = $500

// Transaction Steps
Step 1: Check if Tate's has enough money
IF Tate's Balance >= $100 THEN continue
ELSE rollback

Step 2: Deduct from Tate's account
Tate's Balance = Tate's Balance - $100 // Now $900

Step 3: Add to Zuck's account
ERROR: Network Connection Failed! ❌

// Since Step 3 failed, everything rolls back!
ROLLBACK TRANSACTION

// Final State (Same as Initial)
Tate's Balance = $1000 ✓ (Rolled back to original)
Zuck's Balance = $500 ✓ (Never changed)
Transaction Failed ❌

This is an atomic failure case where we didn’t commit the transaction because one of the DB operations under the transaction block failed.
We successfully verified Tate’s account balance and deducted the amount. However, during the crediting to Zuck’s account, there was a network failure, which caused us to roll back the whole transaction. Now the state of the database remains unchanged despite the failure, and we can say it has passed the atomicity test.

Case 3: Non-Atomic Failure

Before Transaction:
Tate’s balance: $1000
Zuck’s balance: $500

Tate’s failed to send $100 to Zuck’s

After Transaction:
Tate’s balance: $900
Zuck’s balance: $500

CASE 3: NON-ATOMIC FAILURE (Dangerous!)

NO TRANSACTION // No transaction block - each operation runs independently
// Initial State
Tate's Balance = $1000
Zuck's Balance = $500

// Operations run separately
Step 1: Check if Tate's has enough money
IF Tate's Balance >= $100 THEN continue ✓

Step 2: Deduct from Tate's account
Tate's Balance = Tate's Balance - $100 // Now $900 ✓

Step 3: Add to Zuck's account
ERROR: Network Connection Failed! ❌

// Cannot rollback because no transaction block!
// Money is lost in the system! 💸

// Final State (Inconsistent!)
Tate's Balance = $900 ✓ (Money gone!)
Zuck's Balance = $500 ✓ (Never received)
Money Lost: $100 ❌

In a non-atomic operation, we first validate Tate’s account balance and deduct the amount. During the attempt to credit Zuck’s account, a network failure causes the DB operation to fail. Since we’re not using a transaction, we can’t roll back the previous operation. This leads to an inconsistent state where the amount is debited from the source account but not credited to the target account.

CONSISTENCY

Ensured data always moved from one valid state to another following all DB defined rules. rules can be constraints, cascades, triggers, or other defined rules.
Let’s understand it by example suppose we have a DB table account_balances.

CREATE TABLE account_balances (
tate_balance INTEGER,
zuck_balance INTEGER,
-- Total money in system must always be $1000
CHECK (tate_balance + zuck_balance = 1000),
-- No negative balance
CHECK (tate_balance >= 0),
CHECK (zuck_balance >= 0)
);

We have defined two columns tate_balance & zuck_balance. additionally, we have defined a set of DB rules
1. tate_balance + zuck_balance should always be $1000
2. tate_balance should be greater or equal to 0
3. zuck_balance should be greater or equal to 0

Case 1: Consistency Success

Before Transaction:
Tate’s balance: $600
Zuck’s balance: $400

Tate’s send $200 to Zuck’s

After Transaction:
Tate’s balance: $400
Zuck’s balance: $600

START TRANSACTION
Initial State:
Tate's Balance = $600
Zuck's Balance = $400
System Total = $1000 ✓

// Tate's sends $200 to Zuck's
Step 1: Deduct from Tate's
$600 - $200 = $400 ✓

Step 2: Add to Zuck's
$400 + $200 = $600 ✓

Final Check: $400 + $600 = $1000 ✓
COMMIT

Final State:
Tate's: $400
Zuck's: $600
System Total = $1000 ✓

Starting Tate’s balance is $600 & Zuck’s balance is $400, as the sum of both balances is $1000 which follows the DB defined rule we can conclude our DB is in a consistent state.
When Tate’s sends $200 to Zuck’s to escape the matrix, the balance updates show Tate’s at $400 and Zuck’s at $600. The sum remains $1000, maintaining the database rules. This transaction is consistent because it moves from one valid state to another.

Case 2: Consistency Violation

Before Transaction:
Tate’s balance: $600
Zuck’s balance: $400

Tate’s failed to send $700 to Zuck’s

After Transaction:
Tate’s balance: $600
Zuck’s balance: $400

START TRANSACTION
Initial State:
Tate's Balance = $600
Zuck's Balance = $400
System Total = $1000 ✓

// Tate's tries to send $700 to Zuck's
Step 1: Deduct from Tate's
$600 - $700 = -$100 ❌
(Would violate no negative balance rule!)

ROLLBACK

Final State:
Tate's: $600 (Unchanged)
Zuck's: $400 (Unchanged)
System Total = $1000 ✓

When Tate’s became desperate enough to break free from the matrix, he tried to send $700, thinking he could bypass database rules just like he breaks real-world laws.
After the transaction, Tate’s balance should be (-ive)$100 & Zuck’s balance should be $1100 which still follows the $1000 sum rule. However, we have a greater than zero balance validation rule that doesn’t allow the transaction to go through.
So DB declined the transaction as it was violating the consistent state.

ISOLATION

Ensured concurrent transactions were always executed in a consistent manner. it should have the same effect as it’s executing sequentially.
Failure of any concurrent transaction should not move the DB to an inconsistent or incorrect state.
During concurrent transactions on common rows, only one transaction should be able to execute & others should wait for it to complete. if the failure happens during execution it should roll back to the previous consistent state.

Case 1: Concurrent Transaction Success

Before Transaction:
Tate’s balance: $1000
Zuck’s balance: $500

Tate’s send $100 & Zuck’s send $200 concurrently

After Transaction:
Tate’s balance: $1100
Zuck’s balance: $400

// Transaction 1 Starts and Completes First
START TRANSACTION T1
Initial State:
Tate's Balance = $1000
Zuck's Balance = $500
System Total = $1500 ✓

// Tate's sends $100 to Zuck's
Step 1: Deduct from Tate's
$1000 - $100 = $900 ✓

Step 2: Add to Zuck's
$500 + $100 = $600 ✓

Final Check: $900 + $600 = $1500 ✓
COMMIT T1

// Transaction 2 Starts After T1 Completes
START TRANSACTION T2
Initial State:
Tate's Balance = $900
Zuck's Balance = $600
System Total = $1500 ✓

// Zuck's sends $200 back to Tate's
Step 1: Deduct from Zuck's
$600 - $200 = $400 ✓

Step 2: Add to Tate's
$900 + $200 = $1100 ✓

Final Check: $1100 + $400 = $1500 ✓
COMMIT T2

Final State:
Tate's: $1100
Zuck's: $400
System Total = $1500 ✓

During the concurrent transactions, isolation ensures only one transaction gets executed & others wait for its completion.
There are two transactions T1 & T2 to be processed concurrently to maintain isolation one of the transactions T2 waited for the T1 transaction to commit. after completion of T1, T2 executed successfully. the final state of DB looks consistent.

// Transaction 1 (Completes First)
Initial State:
Tate's: $1000
Zuck's: $500
Total: $1500

T1: Tate's -> Zuck's $100
1. Tate's: $1000 - $100 = $900
2. Zuck's: $500 + $100 = $600
3. Total Check: $900 + $600 = $1500 ✓
4. COMMIT ✓

// Transaction 2 (Starts After T1)
T2: Zuck's -> Tate's $200
1. Zuck's: $600 - $200 = $400
2. Tate's: $900 + $200 = $1100
3. Total Check: $1100 + $400 = $1500 ✓
4. COMMIT ✓

Final State:
Tate's: $1100
Zuck's: $400
Total: $1500 ✓

Case 2: Concurrent Transaction Invalid

Before Transaction:
Tate’s balance: $1000
Zuck’s balance: $500
Total balance: $1500

Tate’s send $100 & Zuck’s send $200 concurrently

After Transaction:
Tate’s balance: $1100
Zuck’s balance: $300
Total balance: $1400 ($100 lost 🤡)

// Both Transactions Start Together
START TRANSACTION T1
Initial State:
Tate's Balance = $1000
Zuck's Balance = $500
System Total = $1500 ✓

Step 1: T1 - Deduct from Tate's
$1000 - $100 = $900

// T2 Interrupts!
START TRANSACTION T2
Step 2: T2 - Deduct from Zuck's
$500 - $200 = $300

Step 3: T2 - Add to Tate's
$900 + $200 = $1100
COMMIT T2

Step 4: T1 - Try to Add to Zuck's
$300 + $100 = $400
ERROR! T1 FAILS! ❌

// Problem: Can't rollback T1 because T2 already modified data!
// T2 saw intermediate state from uncommitted T1
// Data integrity compromised!

Corrupted State:
Tate's: $1100
Zuck's: $300 // Lost $100 in the system!
System Total = $1400 ❌

If there is no isolation in concurrent transactions let’s check what happens in case of failure.
T1 sends $100 to T2.
T2 sends $200 to T1.
T1 $100 is deducted new balance is $900
T2 $200 is deducted new balance is $300
T1 $200 is credited new balance is $1100
T2 while crediting $100 network failure happened
Unable to rollback as T1 balance is updated by another transaction. now DB data is incorrect & $100 is lost in the system.

// Dangerous Interleaving (Both Start Together)
Initial State:
Tate's: $1000
Zuck's: $500
Total: $1500

T1: Tate's -> Zuck's $100
1. Tate's: $1000 - $100 = $900

T2: Zuck's -> Tate's $200 (Interrupts!)
2. Zuck's: $500 - $200 = $300
3. Tate's: $900 + $200 = $1100
4. COMMIT T2 ✓

T1: Continues...
5. Zuck's: $300 + $100 = $400
6. FAILS! Can't safely complete! ❌

Corrupted State:
Tate's: $1100
Zuck's: $300
Total: $1400 ❌ (Lost $100!)

Problems:
• T2 saw uncommitted data from T1
• Can't rollback T1 (T2 already committed)
• System total no longer maintains $1500

DURABILITY

Ensured once the transaction is committed there should be no data loss in any kind of failure e.g. database crash, outage, .. etc.
It’s important as once the client gets a successful commit from DB the assumption is data is loaded in non-volatile memory. if we are loading data in volatile memory there can be a scenario where during power failure we can lose the updated data.

Transaction: Tate's sends $100 to Zuck's for matrix escape

1. Initial Database State:
Tate's: $1000
Zuck's: $500

2. Transaction Committed Successfully:
• Deduct from Tate's: $1000 -> $900
• Add to Zuck's: $500 -> $600
• System confirms: "Transaction Complete ✓"
• Tate's: "Perfect! Zuck can escape matrix now!"

3. Suddenly... 💥 System Crashes!

4. After System Recovery:
WITH Durability ✓
• Database recovers to committed state
• Tate's: $900
• Zuck's: $600
• Matrix escape funds safe! ✓

WITHOUT Durability ❌
• Transaction lost
• Tate's: Back to $1000
• Zuck's: Still at $500
• Matrix escape plan failed! 😱

Key Points:
- Committed transactions must survive crashes
- Changes should be permanent
- System recovery must maintain committed state
- Users must be able to trust transaction completions

END

Time to end this blog — I hope you enjoyed it! I’m planning to write more blogs in the future so I can get rich and buy a Bugatti. Feel free to follow me and leave some likes to keep this platform engaging.
And finally, I’ll leave you with the question:
What color is your Database?

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response