django-subatomic: Django Subatomic: The library that makes transactions less magical
It breaks Django’s single `atomic()` escape hatch into narrower tools, so developers have to state exactly what they mean when a database boundary matters.
- Subatomic is an anti-magic library that narrows Django’s transaction API so code must declare its boundary instead of guessing it.
- Its real innovation is not a new database primitive, but a stricter mental model for nested transactions, savepoints, and commit hooks.
- The library treats Django test wrappers as a source of illusion and pushes transaction-sensitive code toward production-like checks.
- Its API and internals use negative constraints on purpose, because the safest transaction boundary is the one you cannot blur.
Django’s `atomic()` is convenient for the same reason it is dangerous: it can quietly do the wrong kind of thing for the moment you are in. `django-subatomic` answers that problem by removing the blur. It splits one broad helper into smaller, stricter tools that force you to say whether you are starting a transaction, requiring one, or only creating a local savepoint.
That sounds fussy until you look at the failure mode. The bug class here is not obvious corruption. It is code that passes in tests, looks fine in review, and then behaves differently when nested inside another transaction or wrapped by Django’s test harness.
The useful kind of friction
The library’s philosophy is not to make transaction work easier. It is to make wrong assumptions harder. The docs describe it as splitting Django’s `atomic` into more specific utilities, which is exactly the right framing: explicitness is the product, convenience is the trade-off.
Subatomic splits Django's atomic into a suite of more specific utilities. It offers precise control over transaction logic in Django projects and transaction simulation in tests.
Why `atomic()` was not enough
Django’s built-in `atomic()` is flexible in a way that hides intent. If you call it inside an existing transaction, it can create a savepoint. If you use it in tests, the surrounding `TestCase` machinery can make code feel safely transactional even when the production path is not equivalent.
| Behavior | Django `atomic()` | Subatomic primitive |
|---|---|---|
| Start a new real transaction | May create a transaction or savepoint depending on context | `transaction()` must start a real boundary or fail |
| Require an existing transaction | Not expressed directly | `transaction_required()` makes the requirement explicit |
| Create a local rollback pocket | Implicit when nested | `savepoint()` exists for that exact job |
| Allow nesting by default | Yes | No for `transaction()`, by design |
| Read well in tests | Can be misleading under `TestCase` | Checks are shaped to expose test-suite illusions |
| Misuse as decorator | Common and accepted | Some helpers are constrained to context-manager use only |
That split matters because a transaction is not just a code pattern. It is a promise about who owns failure, who can roll back, and what should happen after commit. Subatomic wants those promises to be visible in the call site, not inferred from framework behavior.
The API is a set of decisions
from django_subatomic.db import transaction, transaction_required, savepoint, transaction_if_not_already
# Start a real transaction. If one already exists, this should not quietly nest.
with transaction():
do_write()
with savepoint():
maybe_partial_failure()
# Require an existing transaction.
@transaction_required
def enqueue_after_commit_work():
register_work()
# Clunky on purpose. Use only when ownership is unclear.
with transaction_if_not_already():
legacy_work()
| Primitive | Meaning | Design signal |
|---|---|---|
| `transaction()` | Start a durable boundary or fail | Ownership is explicit |
| `transaction_required()` | Run only if a real transaction already exists | Dependency is explicit |
| `savepoint()` | Create a local rollback pocket | Scope stays local |
| `transaction_if_not_already()` | Enter only if needed | This is an exception path, not a default |
| `part_of_a_transaction` | Test helper for transaction-shaped code | Tests should simulate reality, not merely wrap it |
The naming is doing real work. `transaction_if_not_already()` is intentionally clunky because a smooth name would make a compromise look like a norm. The library keeps reminding you that ambiguity is usually a code smell.
The constraint engine behind the API
The deepest trick is not in the public helpers. It is in the internal context-manager machinery that refuses to behave like a decorator when that would blur scope. That is a small implementation choice with a big effect: it keeps transaction boundaries attached to the block where they are visible.
The point is locality. If a savepoint can only be used as a context manager, then rollback behavior stays where the human reader can see it. The library does not just guard the database. It guards the shape of the code around the database.
Kraken’s engineering blog gives the scale context for why this matters. As the blog says, "Here at Kraken we have a lot of code written in Python and the Django web framework. Including tests our main monorepo is well over 10 million lines!" That is exactly the kind of environment where small transaction ambiguities become expensive.
Here at Kraken we have a lot of code written in Python and the Django web framework. Including tests our main monorepo is well over 10 million lines! When we exclude blank lines and comments, there are 5 million lines of Python that we run Mypy over.
Testing without lying to yourself
The testing story is where Subatomic becomes more than a style preference. Django’s `TestCase` wraps each test in a transaction-like shell, which is useful for cleanup but dangerous for signaling. Code that requires a real transaction can appear satisfied when it is only living inside the test harness.
from django_subatomic.test import part_of_a_transaction
from django_subatomic.db import transaction_required
@transaction_required
def must_be_inside_real_transaction():
return "ok"
def test_transaction_required_code():
with part_of_a_transaction():
assert must_be_inside_real_transaction() == "ok"
The helper exists so you can exercise transaction-sensitive code without depending on accidental test-state. That is a valuable discipline. Tests should verify durable behavior, not the illusion of it.
What this says about modern Django
Subatomic is a small library, but it argues for a larger shift in framework thinking. In mature systems, flexibility often becomes a liability because it makes the boundary between safe and unsafe behavior too easy to blur. The answer is not more magic. It is better boundaries.
| Design goal | Django `atomic()` | Subatomic |
|---|---|---|
| Primary value | Convenience and flexibility | Explicitness and correctness |
| Developer burden | Low upfront friction | Higher upfront precision |
| Failure mode | Ambiguous nesting and misleading tests | Earlier errors and fewer hidden assumptions |
| Mental model | One helper that adapts | Several helpers that state intent |
| Scaling posture | Framework magic tolerated | Framework magic contained |
That is why the project feels less like a helper library and more like an opinionated boundary system. It does not replace Django’s transaction model. It narrows it until the behavior is hard to misread.





