skip to content

Databases & Transactions

Django's connection and transaction layer: DATABASES backends, transaction.atomic and on_commit, select_for_update, routers and PostgreSQL extras. Interviewers probe where a write can be lost.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

26

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
open as a page

In Django, what does the DATABASES setting define, and what do you change to move a prototype from SQLite to PostgreSQL?

level: juniorimportance: must knowfreq 60%

basics

~20 s

DATABASES maps aliases such as default to connection settings: ENGINE picks the backend, NAME the database, plus USER, PASSWORD, HOST, PORT and OPTIONS. Moving to PostgreSQL means installing psycopg, setting ENGINE to django.db.backends.postgresql with credentials, and running migrate.

open as a page

In Django, what does transaction.on_commit() do, and why send an order's receipt email through it rather than straight after save()?

level: juniorimportance: must knowfreq 55%

basics

~20 s

transaction.on_commit() registers a callable to run after the current transaction commits and drops it if the transaction rolls back. Sending the receipt from it means no customer is emailed about an order that was never saved.

open as a page

In Django, how does select_for_update() stop two flash-sale buyers from both taking the last unit, and why must it run inside transaction.atomic()?

level: juniorimportance: must knowfreq 58%

basics

~20 s

select_for_update() makes the query lock the rows it returns until the transaction ends, so a second buyer's read waits until the first commits. Outside atomic, autocommit would release the lock at once, so Django raises TransactionManagementError.

open as a page

Why can a Django project whose tests all pass on SQLite still break when it runs on PostgreSQL or MySQL in production?

level: middleimportance: must knowfreq 55%

basics

~20 s

The ORM hides SQL syntax, not engine behaviour: SQLite matches strings differently, stores decimals as floats, ignores select_for_update(), rebuilds tables for schema changes and serialises writers, so code tested only on SQLite can fail or change results on PostgreSQL or MySQL.

open as a page

In Django, why does catching IntegrityError inside a transaction.atomic block and carrying on lead to TransactionManagementError, and where should the try/except go?

level: seniorimportance: must knowfreq 55%

basics

~20 s

When an ORM write fails inside atomic, Django marks the transaction as needing rollback; the next query raises TransactionManagementError and the block rolls back on exit. Put the try/except around an inner atomic block that wraps only the risky write.

open as a page

In Django, how do you store and filter a recipe's list of tags with contrib.postgres's ArrayField, and what are its common pitfalls?

level: juniorimportance: should knowfreq 40%

basics

~10 s

ArrayField(models.CharField(max_length=30)) stores a Python list in one PostgreSQL array column. Filter with tags__contains (has all), tags__overlap (has any), tags__contained_by and tags__len; use default=list, and add a GinIndex on large tables.

open as a page

In Django with several DATABASES aliases, how do you choose which database a QuerySet, a save() or a delete() runs against?

level: juniorimportance: should knowfreq 35%

basics

~10 s

Call using('alias') on a QuerySet, or pass using='alias' to Model.save() and Model.delete(). A manual choice overrides any router; without one, Django asks DATABASE_ROUTERS and falls back to the 'default' alias.

open as a page

What does Django's ATOMIC_REQUESTS database setting wrap in a transaction, what stays outside it, and when do you opt a view out?

level: middleimportance: should knowfreq 44%

basics

~20 s

ATOMIC_REQUESTS, set per database and False by default, wraps each view call in transaction.atomic: commit if the view returns, rollback if it raises. Middleware, template response rendering and streaming output run outside; non_atomic_requests opts a view out.

open as a page

In Django, what happens when transaction.atomic blocks are nested, and how does a failing inner block affect the outer transaction?

level: middleimportance: should knowfreq 52%

basics

~20 s

Only the outermost atomic block opens a transaction; each nested block creates a savepoint. An inner failure rolls back to that savepoint, and if the exception is caught outside the inner block the outer transaction can continue and commit.

open as a page

In Django, what do the CONN_MAX_AGE and CONN_HEALTH_CHECKS database settings control, and what are their defaults?

level: middleimportance: should knowfreq 48%

basics

~20 s

CONN_MAX_AGE is how long, in seconds, a thread keeps its database connection for reuse across requests (default 0, closed after every request; None means unlimited); CONN_HEALTH_CHECKS, default False, checks a reused connection before a request's first query.

open as a page

In Django, when exactly does a transaction.on_commit() callback registered inside a nested atomic block run, and when is it dropped?

level: middleimportance: should knowfreq 38%

basics

~20 s

A callback registered in a nested atomic block runs only after the outermost block commits. If the inner block rolls back to its savepoint, callbacks registered inside it are dropped while earlier ones survive; a full rollback drops them all.

open as a page

In Django, what does adding 'django.contrib.postgres' to INSTALLED_APPS do, and which of its features also need a PostgreSQL extension created in a migration?

level: middleimportance: should knowfreq 30%

basics

~20 s

The app registers the search, trigram and unaccent lookups on CharField and TextField and hstore handling on each connection. HStoreField, trigram lookups, unaccent and GiST equality in ExclusionConstraint also need extensions, added with HStoreExtension, TrigramExtension, UnaccentExtension or BtreeGistExtension.

open as a page

In Django, what do a database router's db_for_read, db_for_write, allow_relation and allow_migrate methods decide, and what does returning None mean?

level: middleimportance: should knowfreq 38%

basics

~20 s

Routers listed in DATABASE_ROUTERS pick an alias for reads and writes, approve relations and approve migrations. Returning None defers to the next router; if none answers, Django uses the hint instance's database or 'default', allows same-database relations and allows migrations.

open as a page

In Django's select_for_update(), what do nowait, skip_locked, of and no_key change, and when would a flash-sale service use each?

level: middleimportance: should knowfreq 38%

basics

~20 s

nowait=True raises DatabaseError instead of waiting; skip_locked=True silently skips locked rows; of=(...) limits locking to named models in a select_related join; no_key=True takes PostgreSQL's weaker FOR NO KEY UPDATE lock that still lets other rows reference the locked one.

open as a page

Moving a Django prototype to PostgreSQL, when would you use OPTIONS={'pool': ...} instead of CONN_MAX_AGE, and what constraints come with it?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Since Django 5.1, OPTIONS['pool'] gives PostgreSQL a psycopg connection pool shared by a process's threads, which suits ASGI and threaded servers; it needs psycopg 3 with psycopg-pool, requires CONN_MAX_AGE = 0, and its size times the process count must fit the server's connection limit.

open as a page

In Django, why can a background task enqueued inside transaction.atomic() fail to find the order it was given, and how does on_commit() fix it?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A task handed to a separate worker inside an atomic block can run before the transaction commits, so the worker cannot see the new row, or the order may roll back. Enqueue it with transaction.on_commit(partial(task.enqueue, order_id=order.pk)).

open as a page

A Django recipe site books cooking-class kitchen stations; how do you stop two active bookings for one station overlapping in time with ExclusionConstraint?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Add an ExclusionConstraint to Meta.constraints over a DateTimeRangeField with RangeOperators.OVERLAPS and the station with RangeOperators.EQUAL, conditioned on active bookings. PostgreSQL then rejects any overlapping insert or update with IntegrityError, even under concurrent requests.

open as a page

A Django marketplace adds a router sending every read to a 'replica' alias for analytics; checkout now raises DoesNotExist reading back an order created inside transaction.atomic() - why, and how do you fix it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The router sends the read to the replica connection, which is outside the transaction that atomic() opened on 'default', so the uncommitted order is invisible there. Scope the router to analytics models and keep reads on the primary inside transactions or with using('default').

open as a page

In Django, why can two flash-sale checkouts that lock several product rows with select_for_update() deadlock, and how do you prevent and handle it?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Checkouts locking the same product rows in opposite orders wait on each other until the database aborts one. Lock all rows in one select_for_update() query ordered by primary key, and retry the whole atomic block on that error.

open as a page

In Django, when does a version column checked in a filtered update() beat select_for_update() for flash-sale stock, and how is a conflict detected?

level: seniorimportance: should knowfreq 42%

basics

~20 s

A filtered update() including the version you read changes the row only if nobody else did; a return value of 0 signals a conflict. It beats select_for_update() when conflicts are rare or read and write are far apart.

open as a page

In Django with several DATABASES aliases, how do migrate --database and a router's allow_migrate() decide which tables are created on each database?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

migrate works on one alias per run, 'default' unless --database names another. For each operation it asks allow_migrate(db, app_label, model_name); a False skips that operation silently, yet the migration is still recorded as applied on that alias.

open as a page

In Django, what does transaction.atomic(durable=True) guarantee, and why does it raise RuntimeError when it is nested inside another block?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

atomic(durable=True) guarantees the block is the outermost one, so its changes are committed when it exits without error. Nesting it would turn it into a savepoint a caller could still roll back, so Django raises RuntimeError instead.

open as a page

In Django, what happens when one of several transaction.on_commit() callbacks raises an exception, and what does robust=True change?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

A raising on_commit callback cannot undo the commit, but by default its exception propagates from the atomic block's exit and the callbacks registered after it never run. With robust=True the exception is logged and the next callbacks still run.

open as a page

Recipe search in a Django app misses misspellings like 'lazagna'; how do you add typo-tolerant matching with trigram_similar or TrigramSimilarity, and index it?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

Create pg_trgm with TrigramExtension, filter with title__trigram_similar and order by a TrigramSimilarity annotation. Add a GinIndex with the gin_trgm_ops operator class; the lookup's operator can use it, while filtering on an annotated similarity score cannot.

open as a page