DEV Community

Cover image for ACID Operation in Backend Engineering: Building Data Integrity Like a Pro
Adeniyi Olanrewaju
Adeniyi Olanrewaju

Posted on

ACID Operation in Backend Engineering: Building Data Integrity Like a Pro

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:

  1. Subtract $300 from Alice’s balance.

  2. 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() 
Enter fullscreen mode Exit fullscreen mode

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") 
Enter fullscreen mode Exit fullscreen mode

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() ) 
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

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)