{"post":{"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","content":"<span class=\"dropcap\">T</span>ransactions are the contract a database makes with your application: either all of these writes happen, or none of them do. Without transactions, a crash at the wrong moment leaves your data in a half-written, inconsistent state. This 2026 guide explains ACID from first principles through to the isolation levels that real production apps actually use.\n\n## What changed in 2026\n\n- **Distributed transactions** got more practical with Postgres logical replication and CockroachDB's serializable guarantees across nodes — but they still come with a latency cost.\n- **ORMs improved transaction ergonomics.** Prisma, Drizzle, and SQLAlchemy all expose clean transaction APIs that avoid the footgun of auto-commit.\n- **Neon's branching** lets you test transactions against a copy of production data without snapshots or backups.\n- **Serializable Snapshot Isolation (SSI)** in Postgres remains the gold standard for correctness without lock contention, and more teams discovered it this year.\n\n## ACID: what each letter actually means\n\n| Property | What the DB guarantees | What breaks without it |\n| --- | --- | --- |\n| Atomicity | All writes in a transaction commit or none do | Partial writes on crash |\n| Consistency | Constraints (FK, CHECK, UNIQUE) are enforced | Referential integrity violations |\n| Isolation | Concurrent transactions do not see each other's uncommitted data | Dirty reads, phantom rows |\n| Durability | Committed data survives crash | Lost writes |\n\n## Isolation levels\n\nPostgres offers four isolation levels. Only three matter in practice:\n\n```sql\n-- Default: good for most reads and non-critical writes\nBEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;\n\n-- Snapshot: each statement sees a consistent snapshot from start of transaction\nBEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;\n\n-- Serializable: transactions behave as if run one at a time\nBEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;\n```\n\n| Level | Dirty read | Non-repeatable read | Phantom read | Throughput |\n| --- | --- | --- | --- | --- |\n| Read Uncommitted | Possible | Possible | Possible | Highest |\n| Read Committed (default) | No | Possible | Possible | High |\n| Repeatable Read | No | No | Possible* | Medium |\n| Serializable | No | No | No | ~10–30% lower |\n\n*Postgres Repeatable Read actually prevents phantoms too, but the SQL standard does not guarantee it.\n\n## A practical transaction\n\n```sql\nBEGIN;\n\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\nUPDATE accounts SET balance = balance + 100 WHERE id = 2;\n\n-- Only commit if both succeeded\nCOMMIT;\n\n-- On error, the application calls:\n-- ROLLBACK;\n```\n\nIf the process dies after the first UPDATE and before COMMIT, the database rolls back automatically. This is atomicity in action.\n\n## Optimistic vs pessimistic locking\n\n**Pessimistic locking** (`SELECT ... FOR UPDATE`) locks the row immediately and holds it until COMMIT. Best when contention is high and conflicts are expected.\n\n```sql\nBEGIN;\nSELECT balance FROM accounts WHERE id = 1 FOR UPDATE;\n-- row is locked; other transactions block here\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\nCOMMIT;\n```\n\n**Optimistic locking** does not lock; it re-checks a version column before writing. Best when contention is low.\n\n```sql\n-- Read with version\nSELECT balance, version FROM accounts WHERE id = 1;\n\n-- Write only if version unchanged\nUPDATE accounts\nSET balance = balance - 100, version = version + 1\nWHERE id = 1 AND version = :expected_version;\n\n-- If 0 rows affected, someone else updated — retry in application\n```\n\n## Savepoints for partial rollback\n\n```sql\nBEGIN;\nINSERT INTO orders (user_id, total) VALUES (42, 99.99);\nSAVEPOINT order_created;\n\nINSERT INTO order_items (order_id, sku) VALUES (1, 'WIDGET');\n-- If this fails:\nROLLBACK TO SAVEPOINT order_created;\n-- order row is preserved; item insert is undone\n\nCOMMIT;\n```\n\nSavepoints let you roll back part of a transaction without aborting the whole thing — useful in complex batch operations.\n\n## How to pick an isolation level\n\n1. **CRUD web app, most reads** → Read Committed (default). Fine for 95% of workloads.\n2. **Report that reads multiple tables and must be consistent** → Repeatable Read. Prevents seeing different snapshots mid-report.\n3. **Money movement, inventory reservation, ticket booking** → Serializable. The cost is worth the correctness guarantee.\n4. **Anything with a SELECT + conditional UPDATE pattern** → Use `SELECT FOR UPDATE` or Serializable to avoid lost updates.\n\n## Common mistakes\n\n**Catching exceptions and continuing inside a transaction.** Once Postgres reports an error, the transaction is aborted. Every subsequent statement returns `ERROR: current transaction is aborted`. You must ROLLBACK and start fresh.\n\n**Long-running transactions.** A transaction open for minutes holds locks and prevents VACUUM from cleaning dead tuples. Keep transactions short — milliseconds, not seconds.\n\n**Deadlocks from inconsistent lock order.** Two transactions each lock row A then try to lock row B (and vice versa). Always acquire locks in a consistent global order to avoid deadlock cycles.\n\n**Autocommit in ORMs.** Many ORMs default to autocommit, meaning each query is its own transaction. Explicitly open a transaction for any multi-step write.\n\n**Read-only queries inside a transaction.** Unnecessary, adds overhead. Simple SELECTs outside a transaction are fine unless you need a consistent snapshot.\n\n## What to skip\n\n- **Read Uncommitted** — it does not exist as a real isolation level in Postgres (it is silently promoted to Read Committed). Avoid it entirely.\n- **Distributed transactions (2PC) for non-critical writes** — the latency and failure complexity rarely justify the consistency guarantee for most apps.\n- **Holding transactions open across HTTP requests** — user think time plus network latency will starve your connection pool.\n\n## FAQ\n\n**What is the difference between a rollback and an abort?**\nThey are the same outcome — all writes in the transaction are undone. \"Abort\" is the database term; \"rollback\" is the application command that triggers it.\n\n**Can I nest transactions in Postgres?**\nNot truly. Postgres has SAVEPOINTS for partial rollback, but nested `BEGIN` calls are ignored. Use savepoints for sub-transaction semantics.\n\n**How do I detect a deadlock?**\nPostgres raises `ERROR: deadlock detected` and rolls back one of the transactions. Log for this error, then retry the transaction in your application.\n\n**Does every ORM handle transactions correctly?**\nNo. Always read your ORM's transaction docs. Drizzle and Prisma are explicit; some legacy ORMs silently autocommit. Test with a forced error to verify rollback behavior.\n\n## Where to go next\n\n- [SQL joins explained in 2026](/blog/sql-joins-explained-2026)\n- [Indexes explained in 2026](/blog/indexes-explained-2026)\n- [How to write a database migration in 2026](/blog/how-to-write-a-database-migration-2026)","hue":310,"metaTitle":"Database Transactions Explained 2026: ACID & Isolation","metaDescription":"2026 guide to database transactions: ACID properties, isolation levels, deadlocks, optimistic vs pessimistic locking, and practical Postgres examples.","keywords":["database transactions explained 2026","acid transactions sql","isolation levels postgres 2026","serializable vs read committed","optimistic locking vs pessimistic","deadlock prevention database","postgres transaction tutorial","database concurrency 2026","savepoint sql","transaction rollback strategy"],"tldr":["**ACID guarantees** — Atomicity, Consistency, Isolation, Durability — are what separate a database from a file.","**Read Committed is the default in Postgres/MySQL** — fine for most apps, but it allows non-repeatable reads.","**Serializable is the safest** — but costs 10–30% throughput; use it only for financial writes.","**Optimistic locking beats pessimistic for low-contention rows** — fewer blocked queries, higher throughput.","**Skip:** wrapping every SELECT in a transaction — read-only queries outside a transaction are fine."],"readTime":"5 min read","wordCount":1080},"related":[{"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","readTime":"6 min read","hue":318},{"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":"threads-vs-processes-explained-2026","title":"Threads vs processes explained in 2026: which isolation model to choose","excerpt":"Threads share memory and are cheap; processes are isolated and safer. In 2026, virtual threads and lightweight containers blur the line further.","category":"code","date":"April 21, 2026","author":"ByteLedger Team","readTime":"6 min read","hue":316},"older":{"slug":"vector-embeddings-explained-2026","title":"Vector embeddings explained in 2026: the practical guide for engineers","excerpt":"Vector embeddings turn text, images, and code into numbers that capture meaning. In 2026 they underpin semantic search, RAG, and recommendations — here is how they actually work and when to use them.","category":"ai","date":"April 21, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":198}}