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.
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
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 = Trueeverywhere without thinking about long-running views holding transactions.
Key points
transaction.atomic()makes multi-step writes all or nothing.- Conditional
update()withF()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.