transaction.atomic() and nested blocks
coreintermediateDjango runs in autocommit mode by default — every query commits immediately on its own. transaction.atomic() (as a decorator or with block) groups everything inside into one real transaction: all-or-nothing, committed together on success, rolled back together on any exception. A NESTED atomic() block creates a SAVEPOINT, not a separate transaction — if the inner block raises, only ITS changes roll back (to the savepoint); the outer block continues and can still commit its own, earlier work.
Think of it as
Picture atomic() as a boundary you draw around a group of writes that must succeed or fail as one unit — a balance transfer's two UPDATEs are the textbook case: either both happen or neither does. Nesting atomic() blocks doesn't create a second, independent transaction — SQL doesn't really have "transactions inside transactions." Instead, Django uses SAVEPOINTs: a marker inside the one real transaction that lets JUST the code since that marker be undone, while everything before it stays intact and can still commit normally when the OUTER block finishes. This is what makes "try this optional step, but keep going even if it fails" possible — wrap the optional step in its own inner atomic(), catch the exception outside it, and the outer transaction's earlier work survives.
What we're doing: Transfer funds between two accounts atomically, and separately attempt an optional bonus-points award that should not undo the transfer if it fails.
- 2
- The outer atomic() is the real transaction boundary — the two balance updates commit or roll back together.
- 8
- The inner atomic() around award_loyalty_points() creates a savepoint — if that call raises, only ITS work rolls back, not the two balance updates that already ran inside the outer block.
Why this works: Without the inner atomic() block, an exception from award_loyalty_points() would propagate up and roll back the ENTIRE outer transaction — undoing a successful funds transfer just because an unrelated, genuinely optional bonus-points step happened to fail. The nested block is what lets the two concerns fail independently.
Assuming an inner atomic() block's success means it has actually committed to the database
Wrong
Better
What you see: Code assumes that because an inner atomic() block exited without raising, its changes are permanently saved — then a LATER exception in the same outer transaction undoes that "already succeeded" work too, surprising anyone who thought the inner block's exit was the actual commit point.
Why: Only the OUTERMOST atomic() block is a true commit boundary — an inner block finishing without error just means its savepoint was released, not that the data is durably committed. The database only actually commits (making the change permanent) when the outermost atomic() block exits successfully; anything that raises afterward, even in unrelated code, rolls back the whole transaction, inner blocks included.
- Outer atomic() — the true commit boundary — its rollback undoes everything, inner blocks included
- Inner atomic() (savepoint) — its own rollback stays scoped to just its own changes
- Autocommit (no atomic()) — default — every query commits immediately on its own
What rolls back, at which nesting level
Together
Remember: Django defaults to autocommit — atomic() (decorator or context manager) is what groups statements into one real transaction. A NESTED atomic() block is a savepoint, not a separate transaction: its own rollback stays scoped to it, but the OUTERMOST block is the true commit boundary, and its rollback undoes everything, inner blocks included.
See also: the broken transaction trap and retries · on commit and durable · transactions and isolation

