Django from Basics to Production · APIs with Django REST Framework, and Production

Transactions and Concurrent Updates

Use transaction.atomic for multi-step writes, F expressions and conditional updates for counters, and on_commit for side effects.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A checkout that creates an order and decrements stock in one transaction.atomic() block, a conditional update that refuses to oversell even when two requests race, and an email that's queued only after the transaction commits.

The idea

  • transaction.atomic() wraps writes in a transaction: an exception rolls everything back.
  • Concurrent requests can both read "1 in stock". Make the update itself conditional: filter(pk=..., stock__gte=qty).update(stock=F("stock") - qty) returns the number of rows changed. Zero means someone else got there first. (On PostgreSQL, select_for_update() locks rows when you need read-then-write.)
  • transaction.on_commit(fn) runs side effects (emails, tasks) only if the transaction commits.

The code

Atomic checkout without overselling
import sys, types
import django
from django.conf import settings

sys.modules["shop"] = types.ModuleType("shop")       # lets this single file act as the "shop" app
sys.modules["shop"].__file__ = __file__
settings.configure(
    SECRET_KEY="dev-only", DEBUG=False, ALLOWED_HOSTS=["testserver"], ROOT_URLCONF=__name__,
    DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
    INSTALLED_APPS=["django.contrib.contenttypes", "django.contrib.auth", "rest_framework", "shop"],
)
django.setup()

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

class Product(models.Model):
    name = models.CharField(max_length=50)
    stock = models.PositiveIntegerField()
    class Meta: app_label = "shop"

class Order(models.Model):
    product = models.ForeignKey(Product, on_delete=models.PROTECT)
    quantity = models.PositiveIntegerField()
    class Meta: app_label = "shop"

with connection.schema_editor() as editor:
    editor.create_model(Product); editor.create_model(Order)
lamp = Product.objects.create(name="Lamp", stock=1)
outbox = []

class OutOfStock(Exception):
    pass

def checkout(product_id, qty):
    with transaction.atomic():
        order = Order.objects.create(product_id=product_id, quantity=qty)
        updated = Product.objects.filter(pk=product_id, stock__gte=qty).update(stock=F("stock") - qty)
        if updated == 0:
            raise OutOfStock()                      # rolls back the order row too
        transaction.on_commit(lambda: outbox.append(f"email: order {order.pk} confirmed"))
    return order.pk

for customer in ("ana", "ben"):                     # two customers want the last lamp
    try:
        print(customer, "-> order", checkout(lamp.pk, 1))
    except OutOfStock:
        print(customer, "-> out of stock")
print("stock:", Product.objects.get(pk=lamp.pk).stock, "| orders:", Order.objects.count(), "| outbox:", outbox)

Output

ana -> order 1
ben -> out of stock
stock: 0 | orders: 1 | outbox: ['email: order 1 confirmed']

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Checking stock in Python and then saving: two requests both pass the check.
  • Sending emails or enqueuing tasks inside the transaction; if it rolls back, the email is already gone. Use on_commit.
  • ATOMIC_REQUESTS = True everywhere without thinking about long-running views holding transactions.

Key points

  • transaction.atomic() makes multi-step writes all or nothing.
  • Conditional update() with F() prevents races without read-then-write.
  • Side effects go in on_commit.

Further reading: Database transactions · select_for_update

Practice questions

Why use transaction.on_commit for sending emails?

Answer

The callback runs only after a successful commit; if the transaction rolls back, no email is sent about an order that doesn't exist.

How does filter(stock__gte=qty).update(stock=F('stock') - qty) prevent overselling?

Answer

The check and the decrement happen in one SQL UPDATE; if another request already took the stock, the condition fails and zero rows are updated.

When would you use select_for_update()?

Answer

When you must read rows, decide in Python and then write, on a database that supports row locks (PostgreSQL, MySQL): it locks the rows until the transaction ends.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?