Today, you're stepping into the role of a Banking System Engineer! You'll build a money transfer system that millions of people trust with their funds. But there's a challenge - what happens when two people try to withdraw from the same account at the exact same time? Let's find out by creating chaos first, then fixing it!
By the end of this lesson, you will:
Imagine you're building BankDB - a digital banking system. Your system needs to:
Today, you'll build this system from scratch and see why transactions are critical!
ACID ensures your banking system never fails:
Atomicity - "All or Nothing"
Consistency - "Rules Always Apply"
Isolation - "No Interference"
Durability - "Permanent Records"
Without proper concurrency control, this happens:
Account Balance: $1000
User A: Reads balance ($1000)
User B: Reads balance ($1000)
User A: Withdraws $800 (1000 - 800 = 200)
User B: Withdraws $800 (1000 - 800 = 200)
Final Balance: $200 (But $1600 was withdrawn!)
Your bank just lost $600! :emoji:
-- Create the banking database
CREATE DATABASE BankDB;
USE BankDB;
-- Create accounts table
CREATE TABLE accounts (
account_id INT PRIMARY KEY AUTO_INCREMENT,
account_holder VARCHAR(100) NOT NULL,
balance DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT positive_balance CHECK (balance >= 0)
);
-- Create transactions history table
CREATE TABLE transaction_history (
transaction_id INT PRIMARY KEY AUTO_INCREMENT,
from_account INT,
to_account INT,
amount DECIMAL(10, 2) NOT NULL,
transaction_type ENUM('deposit', 'withdrawal', 'transfer') NOT NULL,
status ENUM('pending', 'completed', 'failed') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (from_account) REFERENCES accounts(account_id),
FOREIGN KEY (to_account) REFERENCES accounts(account_id)
);
-- Create test accounts with initial balances
INSERT INTO accounts (account_holder, balance) VALUES
('Alice Johnson', 5000.00),
('Bob Smith', 3000.00),
('Charlie Brown', 1000.00),
('Diana Prince', 10000.00),
('Eve Wilson', 2500.00);
-- Verify our accounts
SELECT * FROM accounts;
Let's simulate what happens WITHOUT proper transaction control:
-- DANGEROUS: Transfer without transaction protection
DELIMITER //
CREATE PROCEDURE unsafe_transfer(
IN p_from_account INT,
IN p_to_account INT,
IN p_amount DECIMAL(10, 2)
)
BEGIN
DECLARE current_balance DECIMAL(10, 2);
-- Check sender's balance
SELECT balance INTO current_balance
FROM accounts
WHERE account_id = p_from_account;
-- Simulate processing delay (where race conditions occur!)
SELECT SLEEP(0.1);
-- Deduct from sender
UPDATE accounts
SET balance = balance - p_amount
WHERE account_id = p_from_account;
-- Add to receiver
UPDATE accounts
SET balance = balance + p_amount
WHERE account_id = p_to_account;
SELECT 'Transfer completed' AS status;
END //
DELIMITER ;
Open two MySQL sessions and run these simultaneously:
Session 1:
CALL unsafe_transfer(1, 2, 4000.00); -- Alice to Bob
Session 2:
CALL unsafe_transfer(1, 3, 4000.00); -- Alice to Charlie
Check the damage:
SELECT * FROM accounts WHERE account_id IN (1, 2, 3);
-- Alice's balance might be negative!
Now let's build a SAFE transfer system:
DELIMITER //
CREATE PROCEDURE safe_transfer(
IN p_from_account INT,
IN p_to_account INT,
IN p_amount DECIMAL(10, 2)
)
BEGIN
DECLARE current_balance DECIMAL(10, 2);
DECLARE exit handler for sqlexception
BEGIN
-- If anything goes wrong, rollback!
ROLLBACK;
SELECT 'Transfer failed - rolled back' AS status;
END;
-- Start transaction
START TRANSACTION;
-- Lock the sender's account for update
SELECT balance INTO current_balance
FROM accounts
WHERE account_id = p_from_account
FOR UPDATE;
-- Check if sufficient funds
IF current_balance < p_amount THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Insufficient funds';
END IF;
-- Perform the transfer
UPDATE accounts
SET balance = balance - p_amount
WHERE account_id = p_from_account;
UPDATE accounts
SET balance = balance + p_amount
WHERE account_id = p_to_account;
-- Record the transaction
INSERT INTO transaction_history
(from_account, to_account, amount, transaction_type, status)
VALUES
(p_from_account, p_to_account, p_amount, 'transfer', 'completed');
-- Commit the transaction
COMMIT;
SELECT 'Transfer successful' AS status;
END //
DELIMITER ;
Create a scenario where two transfers can deadlock:
-- Session 1: Transfer from Alice to Bob
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- Wait before completing...
-- Session 2: Transfer from Bob to Alice
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 2;
-- Now try to update Alice's account
UPDATE accounts SET balance = balance + 100 WHERE account_id = 1;
-- DEADLOCK!
Test different isolation levels:
-- Session 1: Set READ UNCOMMITTED (dirty reads possible)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT * FROM accounts WHERE account_id = 1;
-- Session 2: Make uncommitted change
START TRANSACTION;
UPDATE accounts SET balance = 9999.99 WHERE account_id = 1;
-- Don't commit yet!
-- Back to Session 1: You'll see the uncommitted change!
SELECT * FROM accounts WHERE account_id = 1;
Create procedures for all banking operations:
-- Deposit money
DELIMITER //
CREATE PROCEDURE deposit(
IN p_account_id INT,
IN p_amount DECIMAL(10, 2)
)
BEGIN
START TRANSACTION;
UPDATE accounts
SET balance = balance + p_amount
WHERE account_id = p_account_id;
INSERT INTO transaction_history
(to_account, amount, transaction_type, status)
VALUES
(p_account_id, p_amount, 'deposit', 'completed');
COMMIT;
SELECT CONCAT('Deposited
## Challenge Exercises
### Challenge 1: Stress Test Your Bank
Create a stress testing procedure:
```sql
DELIMITER //
CREATE PROCEDURE stress_test_transfers(
IN num_transfers INT
)
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE from_acc INT;
DECLARE to_acc INT;
DECLARE amount DECIMAL(10, 2);
WHILE i < num_transfers DO
-- Random accounts (1-5)
SET from_acc = FLOOR(1 + RAND() * 5);
SET to_acc = FLOOR(1 + RAND() * 5);
-- Ensure different accounts
WHILE from_acc = to_acc DO
SET to_acc = FLOOR(1 + RAND() * 5);
END WHILE;
-- Random amount (10-500)
SET amount = ROUND(10 + RAND() * 490, 2);
-- Attempt transfer (might fail due to insufficient funds)
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
-- Ignore failures, continue testing
END;
CALL safe_transfer(from_acc, to_acc, amount);
END;
SET i = i + 1;
END WHILE;
-- Show final state
SELECT 'Stress test complete!' AS status;
SELECT * FROM accounts ORDER BY account_id;
END //
DELIMITER ;
-- Run the stress test
CALL stress_test_transfers(100);
Add daily interest calculation with proper locking:
DELIMITER //
CREATE PROCEDURE calculate_daily_interest(
IN interest_rate DECIMAL(5, 4)
)
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE acc_id INT;
DECLARE acc_balance DECIMAL(10, 2);
DECLARE cur CURSOR FOR
SELECT account_id, balance FROM accounts;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
START TRANSACTION;
OPEN cur;
read_loop: LOOP
FETCH cur INTO acc_id, acc_balance;
IF done THEN
LEAVE read_loop;
END IF;
-- Calculate and add interest
UPDATE accounts
SET balance = balance + (balance * interest_rate / 365)
WHERE account_id = acc_id;
END LOOP;
CLOSE cur;
COMMIT;
SELECT CONCAT('Interest calculated at ', interest_rate * 100, '% annual rate') AS status;
END //
DELIMITER ;
Create a comprehensive audit trail:
-- Create audit table
CREATE TABLE audit_log (
audit_id INT PRIMARY KEY AUTO_INCREMENT,
table_name VARCHAR(50),
operation VARCHAR(10),
user VARCHAR(100),
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
old_values JSON,
new_values JSON
);
-- Create trigger for balance changes
DELIMITER //
CREATE TRIGGER audit_balance_changes
AFTER UPDATE ON accounts
FOR EACH ROW
BEGIN
INSERT INTO audit_log (table_name, operation, user, old_values, new_values)
VALUES (
'accounts',
'UPDATE',
USER(),
JSON_OBJECT('account_id', OLD.account_id, 'balance', OLD.balance),
JSON_OBJECT('account_id', NEW.account_id, 'balance', NEW.balance)
);
END //
DELIMITER ;
Keep Transactions Short and Sweet
Order Operations Consistently
-- Always lock accounts in ID order to prevent deadlocks
IF from_account_id > to_account_id THEN
-- Lock in ascending order
SELECT ... FROM accounts WHERE account_id = to_account_id FOR UPDATE;
SELECT ... FROM accounts WHERE account_id = from_account_id FOR UPDATE;
ELSE
-- Lock in ascending order
SELECT ... FROM accounts WHERE account_id = from_account_id FOR UPDATE;
SELECT ... FROM accounts WHERE account_id = to_account_id FOR UPDATE;
END IF;
Isolation Level | Use When | Banking Example |
---|---|---|
READ UNCOMMITTED | Never in banking! | :x: Never - could show wrong balance |
READ COMMITTED | Standard operations | :white_check_mark: Checking balance |
REPEATABLE READ | Multi-step calculations | :white_check_mark: Interest calculations |
SERIALIZABLE | Critical operations | :white_check_mark: End-of-day reconciliation |
SET innodb_lock_wait_timeout = 5; -- 5 seconds
Use Appropriate Indexes
-- Index for quick balance lookups
CREATE INDEX idx_account_balance ON accounts(account_id, balance);
-- Index for transaction history queries
CREATE INDEX idx_transaction_date ON transaction_history(created_at);
CREATE INDEX idx_transaction_accounts ON transaction_history(from_account, to_account);
Batch Operations When Possible
-- Instead of 1000 individual updates
START TRANSACTION;
UPDATE accounts
SET balance = balance * 1.001 -- 0.1% interest
WHERE account_type = 'savings';
COMMIT;
Event Sourcing: Banks often store every action as an event
Distributed Transactions: Modern banks use:
Read Replicas: Separate read and write operations
One. Double-Entry Bookkeeping
-- Every transaction has equal debits and credits
INSERT INTO ledger_entries (account, debit, credit) VALUES
('checking_123', 100.00, 0), -- Debit checking
('savings_456', 0, 100.00); -- Credit savings
-- Sum of debits always equals sum of credits
2. Idempotent Operations
-- Use unique transaction IDs to prevent duplicate processing
INSERT INTO transaction_history (transaction_uuid, ...)
VALUES (UUID(), ...)
ON DUPLICATE KEY UPDATE status = status; -- No-op if exists
3. Compensating Transactions
-- For every operation, have a reversal ready
CREATE PROCEDURE reverse_transfer(IN p_transaction_id INT)
-- Reverses a previous transfer by creating opposite entries
:white_check_mark: ACID Compliance
:white_check_mark: Performance
:white_check_mark: Security
:white_check_mark: Monitoring
Congratulations! You've built a banking system that handles millions in transactions safely. You've experienced firsthand why transactions are critical for data integrity and learned how to handle concurrent access like a pro.
In the next lesson, we'll explore how to optimize these systems for massive scale, learning about sharding, replication, and distributed databases. Your banking system is about to go global! :emoji: , p_amount) AS status; END //
-- Withdraw money CREATE PROCEDURE withdraw( IN p_account_id INT, IN p_amount DECIMAL(10, 2) ) BEGIN DECLARE current_balance DECIMAL(10, 2);
START TRANSACTION;
SELECT balance INTO current_balance FROM accounts WHERE account_id = p_account_id FOR UPDATE;
IF current_balance < p_amount THEN ROLLBACK; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient funds'; END IF;
UPDATE accounts SET balance = balance - p_amount WHERE account_id = p_account_id;
INSERT INTO transaction_history (from_account, amount, transaction_type, status) VALUES (p_account_id, p_amount, 'withdrawal', 'completed');
COMMIT;
SELECT CONCAT('Withdrawn
Create a stress testing procedure:
CODE_BLOCK_11
Add daily interest calculation with proper locking:
CODE_BLOCK_12
Create a comprehensive audit trail:
CODE_BLOCK_13
Keep Transactions Short and Sweet
Order Operations Consistently CODE_BLOCK_14
Isolation Level | Use When | Banking Example |
---|---|---|
READ UNCOMMITTED | Never in banking! | :x: Never - could show wrong balance |
READ COMMITTED | Standard operations | :white_check_mark: Checking balance |
REPEATABLE READ | Multi-step calculations | :white_check_mark: Interest calculations |
SERIALIZABLE | Critical operations | :white_check_mark: End-of-day reconciliation |
Use Appropriate Indexes CODE_BLOCK_16
Batch Operations When Possible CODE_BLOCK_17
Event Sourcing: Banks often store every action as an event
Distributed Transactions: Modern banks use:
Read Replicas: Separate read and write operations
One. Double-Entry Bookkeeping CODE_BLOCK_18
2. Idempotent Operations CODE_BLOCK_19
3. Compensating Transactions CODE_BLOCK_20
:white_check_mark: ACID Compliance
:white_check_mark: Performance
:white_check_mark: Security
:white_check_mark: Monitoring
Congratulations! You've built a banking system that handles millions in transactions safely. You've experienced firsthand why transactions are critical for data integrity and learned how to handle concurrent access like a pro.
In the next lesson, we'll explore how to optimize these systems for massive scale, learning about sharding, replication, and distributed databases. Your banking system is about to go global! :emoji: , p_amount) AS status; END //
-- Get account balance with lock option CREATE PROCEDURE get_balance( IN p_account_id INT, IN p_lock BOOLEAN ) BEGIN IF p_lock THEN SELECT account_holder, balance FROM accounts WHERE account_id = p_account_id FOR UPDATE; ELSE SELECT account_holder, balance FROM accounts WHERE account_id = p_account_id; END IF; END // DELIMITER ;
## Challenge Exercises
### Challenge 1: Stress Test Your Bank
Create a stress testing procedure:
__CODE_BLOCK_11__
### Challenge 2: Implement Account Interest
Add daily interest calculation with proper locking:
__CODE_BLOCK_12__
### Challenge 3: Build a Transaction Audit System
Create a comprehensive audit trail:
__CODE_BLOCK_13__
## Best Practices
### 1. Transaction Design Principles
**Keep Transactions Short and Sweet**
- Lock resources for minimal time
- Do validation BEFORE starting transaction
- Prepare all data before BEGIN
**Order Operations Consistently**
__CODE_BLOCK_14__
### 2. Isolation Level Selection Guide
| Isolation Level | Use When | Banking Example |
|----------------|----------|-----------------|
| READ UNCOMMITTED | Never in banking! | ❌ Never - could show wrong balance |
| READ COMMITTED | Standard operations | ✅ Checking balance |
| REPEATABLE READ | Multi-step calculations | ✅ Interest calculations |
| SERIALIZABLE | Critical operations | ✅ End-of-day reconciliation |
### 3. Deadlock Prevention Strategies
1. **Lock Ordering**: Always acquire locks in the same order
2. **Lock Timeout**: Set reasonable timeouts
__CODE_BLOCK_15__
3. **Retry Logic**: Implement automatic retry for deadlocks
4. **Index Optimization**: Ensure locks are as granular as possible
### 4. Performance Optimization
**Use Appropriate Indexes**
__CODE_BLOCK_16__
**Batch Operations When Possible**
__CODE_BLOCK_17__
## Real-World Applications
### How Real Banks Handle Concurrency
1. **Event Sourcing**: Banks often store every action as an event
- Never update, only append
- Calculate balance by replaying events
- Perfect audit trail
2. **Distributed Transactions**: Modern banks use:
- Two-phase commit for multi-system transactions
- Saga patterns for long-running processes
- Event-driven architectures
3. **Read Replicas**: Separate read and write operations
- Writes go to primary database
- Reads from replicas (slight delay acceptable)
### Common Banking System Patterns
**1. Double-Entry Bookkeeping**
__CODE_BLOCK_18__
**2. Idempotent Operations**
__CODE_BLOCK_19__
**3. Compensating Transactions**
__CODE_BLOCK_20__
## Your Banking System Checklist
✅ **ACID Compliance**
- [ ] All transfers are atomic
- [ ] Balance never goes negative
- [ ] Concurrent transfers don't interfere
- [ ] Committed transactions survive crashes
✅ **Performance**
- [ ] Appropriate indexes on accounts table
- [ ] Transaction history is partitioned by date
- [ ] Lock wait timeout is configured
- [ ] Deadlock retry logic implemented
✅ **Security**
- [ ] All procedures validate inputs
- [ ] Audit trail for all operations
- [ ] Role-based access control
- [ ] Encryption for sensitive data
✅ **Monitoring**
- [ ] Track failed transactions
- [ ] Monitor lock wait times
- [ ] Alert on deadlocks
- [ ] Daily reconciliation reports
## Next Steps
Congratulations! You've built a banking system that handles millions in transactions safely. You've experienced firsthand why transactions are critical for data integrity and learned how to handle concurrent access like a pro.
In the next lesson, we'll explore how to optimize these systems for massive scale, learning about sharding, replication, and distributed databases. Your banking system is about to go global! 🌍