{"post":{"slug":"database-transactions-explained-2026","title":"Database transactions explained in 2026: ACID, isolation, and real examples","excerpt":"Transactions are the mechanism that keeps your database consistent when multiple operations run concurrently. This is the 2026 guide to ACID properties, isolation levels, and the patterns that prevent data corruption.","category":"code","date":"April 13, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">T</span>ransactions are the contract between your application and the database: either all of these operations succeed together, or none of them take effect. Without transactions, a crash between two related writes leaves the database in an inconsistent state. With them, the database guarantees that partial updates never happen — and that concurrent writers see a consistent view of the data. This is how they actually work.\n\n## What changed in 2026\n\n- **Distributed transactions are now viable.** Distributed databases (CockroachDB, YugabyteDB, Spanner) support serializable isolation at global scale with latency that is usable in production — though still 2–5× more expensive than single-region.\n- **Postgres 17 improved lock contention monitoring** — `pg_stat_activity` and `pg_locks` views became easier to query, and the new lock waiter metrics make finding blocked transactions much faster.\n- **Application-level sagas** replaced distributed transactions in microservice architectures — multi-service workflows use compensating transactions rather than two-phase commit.\n- **SQLite WAL mode + concurrent readers** made it a serious choice for embedded and edge databases, with transaction semantics closely matching Postgres.\n\n## The ACID properties\n\n**Atomicity** — A transaction is all-or-nothing. If any operation fails, the entire transaction is rolled back as if it never happened.\n\n```sql\nBEGIN;\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\nUPDATE accounts SET balance = balance + 100 WHERE id = 2;\n-- If the second UPDATE fails, the first is also rolled back\nCOMMIT;\n```\n\n**Consistency** — The database transitions from one valid state to another. Constraints (foreign keys, NOT NULL, CHECK) are enforced at commit time.\n\n**Isolation** — Concurrent transactions behave as if they ran serially. The degree of isolation is configurable (see below).\n\n**Durability** — Once committed, data survives crashes. The write-ahead log (WAL) ensures committed data is flushed to disk before the transaction returns success.\n\n## Isolation levels and what goes wrong\n\n| Level | Dirty Read | Non-Repeatable Read | Phantom Read | Common use |\n| --- | --- | --- | --- | --- |\n| READ UNCOMMITTED | Possible | Possible | Possible | Rarely used |\n| READ COMMITTED | Prevented | Possible | Possible | Default in Postgres, MySQL |\n| REPEATABLE READ | Prevented | Prevented | Possible | MySQL InnoDB default |\n| SERIALIZABLE | Prevented | Prevented | Prevented | Financial, inventory systems |\n\n**Dirty read** — reading uncommitted data from another transaction that may still be rolled back.\n\n**Non-repeatable read** — reading the same row twice in one transaction and getting different values because another transaction committed in between.\n\n**Phantom read** — a query returns different rows on repeat execution because another transaction inserted or deleted rows matching the WHERE clause.\n\n### Setting isolation level\n\n```sql\n-- Postgres: per-transaction\nBEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;\nSELECT balance FROM accounts WHERE id = 1;\n-- balance is stable for the life of this transaction\nCOMMIT;\n```\n\n## Optimistic vs pessimistic locking\n\n**Pessimistic locking** acquires the lock before reading, preventing other writers from modifying the row:\n\n```sql\n-- SELECT FOR UPDATE holds a lock until COMMIT/ROLLBACK\nBEGIN;\nSELECT balance FROM accounts WHERE id = 1 FOR UPDATE;\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\nCOMMIT;\n```\n\nSafe, but blocks concurrent readers and writers. Appropriate when conflicts are likely (high-contention rows).\n\n**Optimistic locking** reads without a lock, then checks at write time whether anyone else modified the row:\n\n```sql\n-- version column tracks modifications\nSELECT id, balance, version FROM accounts WHERE id = 1;\n-- application reads version = 7, computes new balance\n\nUPDATE accounts\nSET balance = 900, version = 8\nWHERE id = 1 AND version = 7;  -- fails if another writer incremented version\n-- If 0 rows affected, retry with fresh read\n```\n\nOptimistic locking scales better for read-heavy workloads with infrequent conflicts.\n\n## Savepoints\n\nSavepoints allow partial rollbacks within a transaction:\n\n```sql\nBEGIN;\nINSERT INTO orders (user_id, total) VALUES (42, 500);\nSAVEPOINT after_order;\n\nINSERT INTO order_items (order_id, product_id) VALUES (1, 99);\n-- Something fails here\n\nROLLBACK TO SAVEPOINT after_order;  -- undo only the items insert\n-- The order row is still in place\n\nCOMMIT;  -- commits the order without items\n```\n\nUseful for complex workflows where one optional step failing should not abort the whole transaction.\n\n## Deadlocks\n\nA deadlock occurs when transaction A holds a lock that B needs, and B holds a lock that A needs. The database detects and resolves this by rolling back one transaction.\n\n```\nT1: UPDATE accounts SET ... WHERE id = 1  (holds lock on row 1)\nT2: UPDATE accounts SET ... WHERE id = 2  (holds lock on row 2)\nT1: UPDATE accounts SET ... WHERE id = 2  (waits for T2)\nT2: UPDATE accounts SET ... WHERE id = 1  (waits for T1) → deadlock\n```\n\n**Prevention:** always acquire locks in a consistent order (e.g., always lock the lower ID first). Keep transactions short to reduce the window for conflicts.\n\n## How to pick isolation level\n\n1. **Typical web app reads and writes?** READ COMMITTED (Postgres default) is correct — you get committed data without the overhead of higher isolation.\n2. **Financial transfers or inventory updates?** REPEATABLE READ or SERIALIZABLE — prevents the phantom reads and non-repeatable reads that cause lost updates.\n3. **Reporting queries that must see a point-in-time snapshot?** REPEATABLE READ within the transaction gives you a consistent view without locking.\n4. **Low-latency, high-concurrency writes with rare conflicts?** Optimistic locking + READ COMMITTED — skip heavy locks, retry on conflict.\n\n## Common mistakes\n\n**External I/O inside transactions.** Sending an email, calling a payment API, or writing to S3 inside a BEGIN/COMMIT block means the transaction stays open while the I/O runs. Rollback cannot undo the email. Do I/O after commit.\n\n**Long-running transactions.** A transaction open for minutes holds locks and accumulates WAL. It blocks autovacuum in Postgres (bloat accumulates), delays replication, and increases deadlock probability.\n\n**Not handling transaction retry.** Under SERIALIZABLE or with optimistic locking, serialization failures (error code `40001` in Postgres) are expected and must be retried. Application code that does not retry will fail intermittently.\n\n**Implicit transactions with ORMs.** Many ORMs start a transaction per request by default. Verify this in your ORM's configuration — implicit long-lived transactions are a common source of lock contention.\n\n## What to skip\n\n- **Two-phase commit (2PC) across microservices** — latency and coordinator failure modes make it painful; use a saga pattern with compensating transactions instead.\n- **SERIALIZABLE for all queries** — the overhead is real; reserve it for operations that genuinely require it.\n- **`FOR UPDATE` on high-contention rows** when optimistic locking would work — row locks become a bottleneck at scale.\n\n## FAQ\n\n**When should I use SERIALIZABLE isolation?**\nFor operations where a phantom read or non-repeatable read could cause a real business problem — inventory reservations, financial double-spend prevention, or any place where \"check then act\" logic must be atomic.\n\n**What happens to a Postgres transaction that is idle for hours?**\nIt holds its locks and prevents autovacuum from reclaiming dead tuples. Postgres 14+ introduced `idle_in_transaction_session_timeout` — set it to a few minutes in production.\n\n**Are NoSQL databases ACID-compliant?**\nSome are. MongoDB has supported multi-document ACID transactions since v4.0. DynamoDB supports transactions within a single partition. But most NoSQL databases default to eventual consistency, not ACID.\n\n**What is a saga pattern?**\nA sequence of local transactions, each publishing an event. If a step fails, the saga executes compensating transactions to undo previous steps. It achieves eventual consistency across services without a distributed transaction.\n\n## Where to go next\n\n- [How to optimize SQL queries in 2026](/blog/how-to-optimize-sql-queries-2026)\n- [Idempotency explained in 2026](/blog/idempotency-explained-2026)\n- [How to handle errors gracefully in 2026](/blog/how-to-handle-errors-gracefully-2026)","hue":318,"metaTitle":"Database Transactions Explained 2026: ACID, Isolation Levels","metaDescription":"Database transactions in 2026: ACID properties, isolation levels from READ COMMITTED to SERIALIZABLE, optimistic vs pessimistic locking, and real SQL examples.","keywords":["database transactions explained 2026","acid properties database","isolation levels sql","serializable vs read committed","optimistic locking vs pessimistic","postgres transactions 2026","database concurrency control","deadlock prevention","transaction best practices","sql transaction isolation"],"tldr":["**ACID guarantees are what make a database trustworthy** — Atomicity, Consistency, Isolation, Durability together prevent partial writes, corruption, and lost updates.","**Isolation level is the key tuning knob** — READ COMMITTED is the default in Postgres and MySQL; SERIALIZABLE is the safest but most expensive.","**Optimistic locking scales better than pessimistic locking** — use a version column and retry on conflict rather than holding row locks.","**Keep transactions short** — long transactions hold locks, block other writers, and increase the risk of deadlocks and replication lag.","**Skip:** doing external API calls, sending emails, or any I/O inside a database transaction; rollback cannot undo those side effects."],"readTime":"6 min read","wordCount":1274},"related":[{"slug":"transactions-explained-2026","title":"Database transactions explained in 2026: ACID, isolation, and beyond","excerpt":"Transactions are the safety net of every database write. This 2026 guide covers ACID, isolation levels, deadlocks, and the patterns that prevent data corruption.","category":"code","date":"April 21, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":310},{"slug":"acid-transactions-explained-2026","title":"ACID transactions explained in 2026: what the guarantees really mean","excerpt":"ACID is four guarantees that make database transactions reliable, but each property has nuance that matters in distributed systems. Here is the real story.","category":"code","date":"April 8, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":322},{"slug":"database-isolation-levels-2026","title":"Database Isolation Levels: The Bugs You Only Get Under Load","excerpt":"Your code is correct when one user runs it. Two users at once produce a result neither would accept. Isolation levels decide which of those anomalies you get.","category":"code","date":"August 12, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":304}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"copilot-vs-chatgpt-for-coding-2026","title":"Copilot vs ChatGPT for Coding: 2026 Compared","excerpt":"Copilot finishes your code inside the editor while ChatGPT reasons through whole problems in chat. Here is how the two actually differ in 2026 and when to use each.","category":"ai","date":"April 13, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":260},"older":{"slug":"deep-work-explained-2026","title":"Deep work explained in 2026: what it is, why it matters, and how to practice it","excerpt":"Deep work is focused, distraction-free effort on cognitively demanding tasks. It is increasingly rare and increasingly valuable. Here is the 2026 guide to what it is and how to actually do it.","category":"guides","date":"April 13, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":140}}