Loading learning content...
Every time you transfer money between bank accounts, place an order on an e-commerce site, or update your profile across multiple database tables, you rely on a fundamental guarantee: the operation either completes entirely or doesn't happen at all. There's no acceptable middle ground where half your money disappears into the void or your order is partially recorded.
This guarantee isn't magical—it's the result of decades of database engineering crystallized into four essential properties known by the acronym ACID: Atomicity, Consistency, Isolation, and Durability. These properties aren't abstract academic concepts; they are the contractual promises that database systems make to application developers, and understanding them deeply is essential for any engineer building systems that matter.
By the end of this page, you will understand each ACID property at a deep level—not just what they mean, but how they manifest in real code, where they come from historically, how databases implement them, and critically, how violations of these properties lead to catastrophic failures in production systems. You'll gain the vocabulary and mental models to reason about transaction correctness like a database engineer.
Before we dive into the properties themselves, it's valuable to understand where they came from. The ACID acronym was coined by Andreas Reuter and Theo Härder in 1983, though the underlying concepts had been developing since the 1970s.
The need for these properties emerged from a fundamental problem: how do you maintain data integrity when multiple users access and modify the same data simultaneously, especially when failures can occur at any moment?
Early database systems in the 1960s and 1970s were plagued by data corruption issues. A power failure during an update could leave data in an inconsistent state. Two users modifying the same record could overwrite each other's changes. The database might report success but lose the data before it reached disk.
The pioneers of database theory—particularly Jim Gray, who would later win the Turing Award—developed the transaction concept as the solution. A transaction wraps multiple operations into a single logical unit with specific guarantees. These guarantees became codified as ACID.
Jim Gray's 1981 paper 'The Transaction Concept: Virtues and Limitations' remains foundational reading. His work on transactions, logging, and recovery mechanisms forms the basis of how all major relational databases operate today. Understanding ACID is understanding Gray's vision of reliable data systems.
Atomicity is derived from the Greek word atomos, meaning "indivisible." In the context of database transactions, atomicity guarantees that a transaction is treated as a single, indivisible unit of work. Either all operations within the transaction complete successfully, or none of them take effect.
This is the property that prevents "half-done" states. Consider a classic bank transfer:
Atomicity guarantees that you can never end up in a state where Account A has been debited but Account B hasn't been credited. If the system crashes after step 1 but before step 2, atomicity ensures that when the system recovers, Account A still has its original balance.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
public class TransferService { private final DataSource dataSource; public void transfer(String fromAccount, String toAccount, BigDecimal amount) throws TransferException { Connection conn = null; try { conn = dataSource.getConnection(); // BEGIN TRANSACTION - Atomicity starts here conn.setAutoCommit(false); // Step 1: Debit source account int debitedRows = debitAccount(conn, fromAccount, amount); if (debitedRows != 1) { throw new TransferException("Failed to debit source account"); } // Step 2: Credit destination account int creditedRows = creditAccount(conn, toAccount, amount); if (creditedRows != 1) { throw new TransferException("Failed to credit destination account"); } // Both steps succeeded - commit the atomic unit conn.commit(); } catch (SQLException | TransferException e) { // Any failure: rollback ensures ATOMICITY // Neither debit nor credit will persist if (conn != null) { try { conn.rollback(); } catch (SQLException re) { // Log rollback failure } } throw new TransferException("Transfer failed, no changes made", e); } finally { closeConnection(conn); } } private int debitAccount(Connection conn, String accountId, BigDecimal amount) throws SQLException { String sql = "UPDATE accounts SET balance = balance - ? " + "WHERE account_id = ? AND balance >= ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setBigDecimal(1, amount); ps.setString(2, accountId); ps.setBigDecimal(3, amount); // Ensure sufficient balance return ps.executeUpdate(); } } private int creditAccount(Connection conn, String accountId, BigDecimal amount) throws SQLException { String sql = "UPDATE accounts SET balance = balance + ? WHERE account_id = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setBigDecimal(1, amount); ps.setString(2, accountId); return ps.executeUpdate(); } }}Databases implement atomicity through a technique called Write-Ahead Logging (WAL). Before any change is applied to the actual data files, the intended change is first written to a transaction log. This log is the source of truth for what should happen.
The sequence is:
Recovery after crash:
This mechanism ensures that even if the system crashes mid-transaction, recovery will restore the database to a consistent state where only complete, committed transactions are visible.
A common misconception is that atomicity prevents other transactions from seeing in-progress changes. It doesn't—that's the job of Isolation (the 'I' in ACID). Atomicity only guarantees that if you roll back, no changes persist. Without proper isolation, other transactions might read your uncommitted changes before you decide to roll back.
Consistency is perhaps the most misunderstood ACID property. In the database context, consistency means that a transaction transforms the database from one valid state to another valid state, preserving all integrity constraints, invariants, and business rules defined on the data.
These constraints include:
Critical insight: While the database enforces many consistency rules automatically, application-level consistency is YOUR responsibility.
| Constraint Type | Enforced By | Example | Failure Behavior |
|---|---|---|---|
| NOT NULL | Database | email column cannot be null | SQL error, transaction fails |
| UNIQUE | Database | email must be unique per user | Constraint violation error |
| FOREIGN KEY | Database | order.customer_id must exist in customers | Referential integrity error |
| CHECK | Database | CHECK (balance >= 0) | Check constraint violation |
| Application Rule | Your Code | User cannot order more than inventory | Must be enforced in application logic |
| Cross-Aggregate | Your Code | Order total must match sum of line items | Application must maintain invariant |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
public class OrderService { /** * This method demonstrates maintaining APPLICATION-LEVEL consistency. * The database can't enforce that order total equals sum of line items; * that's our responsibility. */ public void addLineItem(Connection conn, UUID orderId, LineItem item) throws SQLException, BusinessRuleException { conn.setAutoCommit(false); try { // Step 1: Verify the order exists and is in valid state Order order = findOrderForUpdate(conn, orderId); if (order.getStatus() != OrderStatus.DRAFT) { throw new BusinessRuleException( "Cannot modify order in status: " + order.getStatus() ); } // Step 2: Check inventory availability - APPLICATION CONSISTENCY int availableStock = getAvailableStock(conn, item.getProductId()); int alreadyReserved = getOrderedQuantity(conn, orderId, item.getProductId()); if (availableStock - alreadyReserved < item.getQuantity()) { throw new BusinessRuleException( "Insufficient stock for product: " + item.getProductId() ); } // Step 3: Insert line item insertLineItem(conn, orderId, item); // Step 4: Recalculate order total - MAINTAINING INVARIANT BigDecimal newTotal = calculateOrderTotal(conn, orderId); updateOrderTotal(conn, orderId, newTotal); // Step 5: Reserve inventory - ANOTHER APPLICATION INVARIANT reserveInventory(conn, item.getProductId(), item.getQuantity()); // All consistency rules maintained, commit conn.commit(); } catch (SQLException | BusinessRuleException e) { conn.rollback(); // Restore to previous consistent state throw e; } } /** * Note the "FOR UPDATE" - this locks the row to prevent concurrent * modifications that could break consistency */ private Order findOrderForUpdate(Connection conn, UUID orderId) throws SQLException { String sql = "SELECT * FROM orders WHERE id = ? FOR UPDATE"; // ... execute and map result } private BigDecimal calculateOrderTotal(Connection conn, UUID orderId) throws SQLException { String sql = "SELECT COALESCE(SUM(quantity * unit_price), 0) " + "FROM order_line_items WHERE order_id = ?"; // ... execute and return }}Don't confuse ACID Consistency with CAP Theorem Consistency! ACID Consistency is about preserving data integrity and business rules. CAP Consistency is about all nodes in a distributed system seeing the same data at the same time. These are entirely different concepts that unfortunately share the same name.
Modern application development creates a shared responsibility for consistency:
Database-Enforced Consistency:
Application-Enforced Consistency:
The danger zone: Many consistency violations happen in application code that either doesn't enforce business rules within transaction boundaries, or breaks atomicity by making partial commits.
Isolation is the property that determines how and when the changes made by one transaction become visible to other concurrent transactions. In a perfect world, each transaction would execute as if it were the only transaction running—this is called serializability, the strongest isolation level.
However, strict serializability comes with significant performance costs. Real-world databases offer a spectrum of isolation levels that trade off correctness guarantees against performance and concurrency.
Without proper isolation, concurrent transactions can experience various anomalies—situations where the observed data behavior wouldn't be possible if transactions ran one at a time.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
/** * Demonstration of concurrency anomalies and how isolation levels prevent them */public class IsolationDemonstration { // ================================ // DIRTY READ SCENARIO // ================================ // READ UNCOMMITTED allows this; READ COMMITTED prevents it void dirtyReadScenario() { // Transaction A (Account Holder) // Time T1: Account balance is $1000 // Time T2: Transaction B starts a withdrawal of $500 // UPDATE accounts SET balance = 500 WHERE id = 'A' // (not yet committed) // Time T3: Transaction A reads balance - sees $500 (DIRTY READ!) // Time T4: Transaction B encounters an error and ROLLS BACK // Balance is restored to $1000 // Time T5: Transaction A still thinks balance is $500 // Makes decisions based on PHANTOM DATA } // ================================ // LOST UPDATE SCENARIO // ================================ // Even READ COMMITTED allows this; REPEATABLE READ prevents it void lostUpdateScenario() { // Initial: inventory = 100 // Transaction A (Order #1) // T1: SELECT quantity FROM inventory WHERE product_id = 'X' // Result: 100 // T2: (calculates: 100 - 10 = 90) // Transaction B (Order #2) - CONCURRENT // T3: SELECT quantity FROM inventory WHERE product_id = 'X' // Result: 100 (same as A saw) // T4: (calculates: 100 - 5 = 95) // T5: UPDATE inventory SET quantity = 95 WHERE product_id = 'X' // T6: COMMIT // Transaction A continues // T7: UPDATE inventory SET quantity = 90 WHERE product_id = 'X' // A's update OVERWRITES B's! // T8: COMMIT // Final result: quantity = 90 // LOST: B's update (should have been 85 after both orders) } // ================================ // WRITE SKEW SCENARIO // ================================ // Even REPEATABLE READ allows this in some DBs; SERIALIZABLE prevents it void writeSkewScenario() { // Business rule: At least one doctor must be on call // Currently: Dr. Alice is on call, Dr. Bob is on call (2 total) // Transaction A (Dr. Alice wants to go off-call) // T1: SELECT COUNT(*) FROM on_call WHERE on_call = true // Result: 2 (Alice and Bob) // T2: "2 > 1, so if I leave, Bob is still on call. Safe!" // T3: UPDATE doctors SET on_call = false // WHERE name = 'Alice' // Transaction B (Dr. Bob wants to go off-call) - CONCURRENT // T4: SELECT COUNT(*) FROM on_call WHERE on_call = true // Result: 2 (Alice and Bob - sees snapshot before A's update) // T5: "2 > 1, so if I leave, Alice is still on call. Safe!" // T6: UPDATE doctors SET on_call = false // WHERE name = 'Bob' // Both transactions commit // Result: NOBODY is on call! // Both individually made valid decisions, but combined: INVARIANT VIOLATED }}| Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read | Performance |
|---|---|---|---|---|
| READ UNCOMMITTED | Possible | Possible | Possible | Highest |
| READ COMMITTED | Prevented | Possible | Possible | High |
| REPEATABLE READ | Prevented | Prevented | Possible* | Medium |
| SERIALIZABLE | Prevented | Prevented | Prevented | Lowest |
The SQL standard defines isolation levels, but database vendors implement them differently. PostgreSQL's REPEATABLE READ actually prevents phantom reads (using MVCC snapshots). MySQL/InnoDB's REPEATABLE READ can have phantom reads with some operations. Oracle doesn't even offer true REPEATABLE READ—just READ COMMITTED and SERIALIZABLE. Always test behavior on YOUR database!
Durability provides the guarantee that once a transaction commits, its changes will survive any subsequent failure—including power outages, system crashes, and hardware failures. When your commit call returns success, the data is safe.
This sounds simple, but implementing durability is surprisingly complex. Modern computers have multiple layers of caching (CPU cache, OS buffer cache, disk controller cache), and data isn't truly 'on disk' just because your program wrote it.
The durability challenge:
Databases use several techniques to ensure durability at each layer.
1234567891011121314151617181920212223
-- PostgreSQL durability configuration settings -- Ensure WAL is flushed to disk on every commit (default: on)-- Setting to 'off' risks losing recent transactions after crashSET synchronous_commit = 'on'; -- How much WAL to buffer before forcing a write-- Larger values improve performance but risk more data lossSHOW wal_buffers; -- typically 64MB -- For synchronous replication (extreme durability)-- Commit blocks until replica confirms WAL receiptSET synchronous_commit = 'remote_write'; -- replica received WALSET synchronous_commit = 'remote_apply'; -- replica applied WAL -- Checkpoint behavior-- More frequent checkpoints = faster recovery but lower performanceSHOW checkpoint_timeout; -- default 5 minutesSHOW max_wal_size; -- triggers checkpoint when exceeded -- View current WAL position and replication statusSELECT pg_current_wal_lsn(); -- current write-ahead log positionSELECT * FROM pg_stat_replication; -- replica sync statusDisabling durability settings (async commits, disabled fsync) can improve performance 10-100x but risks real data loss. In 2011, a major cloud provider lost customer data across multiple availability zones because they disabled durability features for performance. The data that was 'committed' but not yet physically written was lost forever when power failed. Understand these trade-offs deeply before touching durability settings.
While we've examined each ACID property individually, they work together as an integrated system of guarantees. Understanding their interplay is crucial for building robust systems.
The lifecycle of a reliable transaction:
| Phase | ACID Property | What Happens | Guarantee Provided |
|---|---|---|---|
| Begin | Isolation | Transaction gets a snapshot or acquires locks | Other transactions won't interfere |
| Execute | Consistency | Operations are validated against constraints | Only valid state transitions allowed |
| Execute | Isolation | Changes are invisible to others (or controlled visibility) | No dirty reads (at appropriate level) |
| Commit | Atomicity | All changes are made permanent together | No partial commits possible |
| Commit | Durability | Changes survive system failures | Committed data is never lost |
| Rollback | Atomicity | All changes are undone completely | No partial rollbacks possible |
Distributed Systems Challenge:
ACID properties are well-defined for single-database transactions. When you need to coordinate transactions across multiple databases, message queues, or microservices, maintaining ACID becomes significantly harder:
This is why patterns like Saga, Outbox, and eventual consistency emerged—they provide weaker but more practical guarantees for distributed coordination.
Application-Level Violations:
Even with a fully ACID-compliant database, your application can break guarantees:
Think of ACID as a contract between you and the database. The database promises to uphold these properties IF you use transactions correctly. If you bypass transactions, mix transactional and non-transactional operations, or ignore errors, the contract is void. ACID is not magic—it requires correct usage.
We've explored the foundational guarantees that make reliable database systems possible. Let's consolidate the essential takeaways:
What's next:
Understanding ACID properties is foundational, but the real skill is applying them in code. The next page examines transaction boundaries in code—how to identify where transactions should begin and end, common patterns for transaction management, and the critical mistakes that break ACID guarantees in real applications.
You now understand the four fundamental ACID properties that govern reliable database transactions. These properties aren't just theory—they directly impact how you design and implement data access code. In the next page, we'll see how to draw transaction boundaries correctly in real applications.