When you build a banking app, an e-commerce checkout, or any critical backend system, data integrity is everything. A single failed transaction could cause duplicate charges, missing payments, or worse.
To prevent such disasters, databases use ACID properties:
- Atomicity
- Consistency
- Isolation
- Durability
In this article, we’ll break each property into plain English explanations with Python code examples using SQLAlchemy.
1. Atomicity – All or Nothing
Atomicity means that a transaction is indivisible.
If part of the transaction fails, the entire operation is rolled back.
Example Scenario
Alice is transferring $300 to Bob:
Subtract $300 from Alice’s balance.
Add $300 to Bob’s balance.
If step 2 fails, step 1 must be undone.
Code Example
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base
Base = declarative_base()
class Account(Base):
__tablename__ = 'accounts'
id = Column(Integer, primary_key=True)
name = Column(String, unique=True)
balance = Column(Integer, default=0)
engine = create_engine("sqlite:///bank.db", echo=False)
Session = sessionmaker(bind=engine)
Base.metadata.create_all(engine)
def atomic_transfer(session, from_id, to_id, amount):
try:
from_acc = session.query(Account).filter(Account.id == from_id).with_for_update().first()
to_acc = session.query(Account).filter(Account.id == to_id).with_for_update().first()
if from_acc.balance < amount:
raise ValueError("Insufficient funds")
from_acc.balance -= amount
to_acc.balance += amount
session.commit() # Only commit if all steps succeed
print("Transfer completed.")
except Exception as e:
session.rollback() # Undo everything if something fails
print("Transfer failed:", e)
# Run a test
session = Session()
atomic_transfer(session, from_id=1, to_id=2, amount=300)
session.close()
2. Consistency – Valid State Before and After
Consistency ensures that data always follows rules.
For example:
Balances cannot go negative.
Total money in the system should remain correct.
Example
# Consistency check
if from_acc.balance < amount:
raise ValueError("Insufficient funds: cannot go negative")
Here, we ensure no transaction breaks the system’s rules.
3. Isolation – Transactions Don’t Interfere
Isolation means that concurrent transactions behave as if they’re running one after another, even when they’re processed at the same time.
Example
Two transfers happening at once:
Alice transfers $200 to Bob.
Bob transfers $100 to Charlie.
Without isolation, their operations might overlap and cause wrong balances.
Code Example
Using with_for_update()
to lock rows during the transfer:
from_acc = (
session.query(Account)
.filter(Account.id == from_id)
.with_for_update()
.first()
)
to_acc = (
session.query(Account)
.filter(Account.id == to_id)
.with_for_update()
.first()
)
This works for a single worker, but when you have multiple workers or parallel requests, isolation gets tricky.
4. Durability – Changes Persist
Durability guarantees that once a transaction is committed, the changes survive crashes.
Example
def durable_deposit(session, account_id, amount):
try:
acc = session.query(Account).filter(Account.id == account_id).with_for_update().first()
acc.balance += amount
session.commit()
print(f"Deposited {amount} to {acc.name}. This change is permanent.")
except Exception as e:
session.rollback()
print("Deposit failed:", e)
Even if your server restarts after session.commit(), the deposit remains in the database.
5. Full Example: Deposit + Transfer
def deposit_and_transfer(session, deposit_to, deposit_amt, transfer_from, transfer_to, transfer_amt):
try:
# Deposit
acc = session.query(Account).filter(Account.id == deposit_to).with_for_update().first()
acc.balance += deposit_amt
print(f"Deposited {deposit_amt} to {acc.name}. New balance: {acc.balance}")
# Transfer
from_acc = session.query(Account).filter(Account.id == transfer_from).with_for_update().first()
to_acc = session.query(Account).filter(Account.id == transfer_to).first()
if from_acc.balance < transfer_amt:
raise ValueError("Insufficient funds for transfer")
from_acc.balance -= transfer_amt
to_acc.balance += transfer_amt
session.commit()
print("Deposit and transfer successful.")
except Exception as e:
session.rollback()
print("Operation failed:", e)
This ensures all steps succeed or all fail.
Why ACID Matters
Atomicity: No partial transfers.
Consistency: No invalid states.
Isolation: No clashing transactions.
Durability: Data persists after commit.
Coming Next: Isolation Under Pressure
Our examples work well when there’s one worker.
But with 4 workers handling two webhooks at the same time, things can break.
We’ll uncover why with_for_update() is not enough and how to use distributed locks or queues to fix it.
Top comments (0)