skip to content

In Django, what does the default autocommit mode mean for a two-wallet transfer, and how does transaction.atomic change it?

level: juniorimportance: must knowfreq 72%

answer

  1. each statement is its own transaction
  2. PEP 249 says off, Django says on
  3. outermost block opens, exit decides
  4. decorator or with-statement

basics

~20 s

Django runs in autocommit, so every ORM query commits on its own. transaction.atomic, used as a decorator or a with-block, runs its queries in one transaction: committed if the block exits normally, rolled back if an exception escapes it.

solid answer

~40 s

Django turns the database connection's autocommit on, so outside any transaction each query is committed the moment it succeeds. A transfer written as two separate `update()` calls is therefore two transactions: if the process dies or raises between them, the debit is permanent and the credit never happens. Wrapping the transfer in `transaction.atomic` — as `@transaction.atomic` on the function or `with transaction.atomic():` around the lines — makes the outermost block open a transaction on entry, commit it when the block exits normally, and roll it back when an exception propagates out; the exception is then re-raised to the caller. `atomic(using=...)` picks a database alias other than `default`. The rollback only touches the database: attributes on model instances you already changed in Python keep their new values.

code

python · 21 lines
python
from decimal import Decimal

from django.db import models, transaction
from django.db.models import F


class Wallet(models.Model):
    owner = models.CharField(max_length=100)
    balance = models.DecimalField(max_digits=12, decimal_places=2)


class InsufficientFunds(Exception):
    pass


@transaction.atomic
def transfer(source_id: int, target_id: int, amount: Decimal) -> None:
    Wallet.objects.filter(pk=source_id).update(balance=F("balance") - amount)
    if Wallet.objects.get(pk=source_id).balance < 0:
        raise InsufficientFunds(source_id)  # both updates are rolled back
    Wallet.objects.filter(pk=target_id).update(balance=F("balance") + amount)

go deeper

for a junior

Recall that Django commits each query immediately by default, and that atomic works as a decorator or a with-block, committing on normal exit and rolling back on an exception.

for a middle

Explain why Django turns autocommit on despite PEP 249, which ORM operations it already wraps internally, and that commit or rollback calls are forbidden inside a block.

for a senior

Show that you size the block to one business operation, keep it short, and deal with state that rollback cannot restore: in-memory instances, caches and external calls.

for a principal

Discuss where a team standardises transaction boundaries, service functions versus views, and how that choice interacts with multiple database aliases that have no shared commit.

## Autocommit is Django's default A **transaction** groups several SQL statements so that the database applies all of them or none of them. The Python database API standard, **PEP 249**, says a connection should start with autocommit *off*, which means every statement silently joins an open transaction that you must commit yourself. Django deliberately overrides that: it switches the connection to **autocommit mode** (the per-database `AUTOCOMMIT` option, default `True`). In autocommit, a query that runs while no transaction is active is wrapped in its own tiny transaction and committed as soon as it succeeds. For single statements that is exactly what you want: `wallet.save()` outside any block is durable the instant it returns. Django also protects its own multi-query operations — a cascading `delete()` or saving a model with multi-table inheritance — by wrapping them internally, so the ORM does not leave half of *one* operation behind. ## Why a transfer breaks under plain autocommit A transfer is *your* multi-statement operation, and Django cannot guess its boundary: - the debit `UPDATE` on the source wallet commits immediately; - a crash, a timeout or a raised exception before the credit leaves money missing; - no later code can "undo" the debit, because it is already committed. ## What `transaction.atomic` does `django.db.transaction.atomic(using=None, savepoint=True, durable=False)` marks a block whose database work must be all-or-nothing. It works in two forms: 1. **Decorator** — `@transaction.atomic` on a function (with or without parentheses); every call runs the whole body in the block. 2. **Context manager** — `with transaction.atomic():` around only the lines that need it, leaving the rest of the function in autocommit. For the **outermost** block the rules are simple: | Moment | What Django does | |---|---| | Entering the block | Turns autocommit off and starts a transaction | | Leaving normally | Commits the transaction | | An exception propagates out | Rolls back, then re-raises the exception | | Leaving either way | Restores autocommit for the connection | Inside the block, calling `transaction.commit()` or `transaction.rollback()` yourself, or changing autocommit, raises `TransactionManagementError` — the block owns the outcome. Nested blocks become savepoints rather than new transactions, which is a separate topic. ## What rollback does not undo - **Python objects**: a `Wallet` instance whose `balance` you changed keeps the changed value after a rollback; re-fetch it or reset the attribute. - **Other systems**: a cache write, an email or an HTTP call made inside the block has already happened; Django's commit hooks exist for that. - **Other databases**: `atomic()` covers one alias; `atomic(using="ledger")` is needed for a second one, and there is no cross-database commit. ## Practical guidance - Put the block around one business operation — here, the whole transfer — not around the entire program. - Keep it short; an open transaction holds locks and a connection. - Raise an exception (for example an `InsufficientFunds` error) to abort; the rollback is automatic. - Stopping another request from reading or changing the same wallets concurrently is a locking question (`select_for_update()`), not something `atomic` alone provides. ## Mistakes interviewers listen for - **"Django wraps every request in a transaction."** Only when `ATOMIC_REQUESTS` is set to `True` for that database; the default is `False`. - **"The ORM batches my changes and commits at the end."** Django has no unit of work: `save()`, `update()` and `delete()` send SQL immediately, and in autocommit it is committed immediately too. - **"I'll call `commit()` at the end of the block."** Not needed and not allowed; the block commits on normal exit. - **"Two saves in one function are atomic."** A function is not a transaction. Without a block, each statement is its own transaction. A strong junior answer names the default (autocommit), the two forms of `atomic`, the commit-or-rollback rule on exit, and one thing rollback does not undo.

  • If the credit fails and the block rolls back, what is the balance attribute on a Wallet instance you modified in Python?
    Still the modified value. A rollback restores rows in the database, not attributes on objects in memory. Code that keeps using the instance after catching the exception should re-read it with `refresh_from_db()` or reset the field, otherwise it acts on a balance that was never committed.
  • Can you call transaction.commit() halfway through an atomic block to make the debit permanent early?
    No. While an `atomic` block is active Django forbids explicit `commit()`, `rollback()` and autocommit changes and raises `TransactionManagementError`. If part of the work must be committed independently, it has to run outside the block, before it starts or after it ends.

saying these in an interview costs you the question

  • Django wraps every request in a transaction by default
  • Queries are held until the end of the view and committed together
  • A rollback also restores the attributes of model instances in memory
  • You must call transaction.commit() at the end of an atomic block
  • Two consecutive save() calls are atomic because they are in one function