>Django Transaction Primitives
django-subatomic splits atomic() into explicit primitives so your code says what it means -- transaction, savepoint, transaction_required, and durable.
Charlie Denton and Sam Searles-Bryant’s talk at DjangoCon Europe 2026 on why transaction.atomic() conflates two different things and how django-subatomic fixes it.
The problem with atomic()
atomic() is ambiguous. The same code creates a transaction OR a savepoint depending on whether it’s already inside a transaction. You can’t tell by reading it.
with transaction.atomic(): # BEGIN
Author.objects.create(...)
with transaction.atomic(): # SAVEPOINT (not a new transaction)
Book.objects.create(...)
# RELEASE SAVEPOINT
# COMMIT
Every nested atomic() creates savepoint queries you probably don’t need. At scale (Kraken: 16M lines of Python), this adds up. And on_commit callbacks can lie — if an outer atomic() wraps your durable=True block, the commit can still be rolled back after your callback fires.
The fix: four primitives
django-subatomic splits atomic() into explicit parts:
| Primitive | What it does |
|---|---|
transaction() | Creates a transaction. Fails if already in one |
savepoint() | Creates a savepoint. Fails if NOT in a transaction |
transaction_required() | Asserts a transaction exists. Creates nothing |
durable() | Asserts code runs outside any transaction |
from django_subatomic import db
with db.transaction():
Account.objects.create(name="Alice", balance=1000)
Account.objects.create(name="Bob", balance=500)
# COMMIT -- both accounts created atomically
The most-used primitive at Kraken is transaction_required — most code needs atomicity guarantees but shouldn’t define the transaction boundary:
@db.transaction_required
def transfer(from_acct, to_acct, amount):
"""Must be called inside a transaction. Won't create one."""
from_acct.balance -= amount
from_acct.save()
to_acct.balance += amount
to_acct.save()
durable prevents side effects inside transactions:
@db.durable
def send_welcome_email(email):
"""Guaranteed to run outside any transaction."""
EmailService.send(email)
# If called inside a transaction, raises immediately
And run_after_commit replaces on_commit but raises if no transaction is open — no silent immediate execution:
with db.transaction():
user = User.objects.create(username="alice")
db.run_after_commit(partial(send_welcome_email, user.email))
# Email sends only after successful COMMIT
Key takeaways
atomic()was a huge improvement but hides whether you’re getting a transaction or a savepoint- Most code needs
transaction_required, notatomic()— you want guarantees without creating boundaries everywhere - Unnecessary savepoints cost real queries — at scale, the extra SQL adds up
- django-subatomic works alongside
atomic(), no big-bang rewrite needed - Production-tested across 100+ environments at Kraken Tech