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.

8 min read • View on GitHub • More from kraken-tech

A constrained machine with one central lever and three smaller gates around it, each gate controlling a different kind of database boundary. The scene explains that this library replaces one flexible transaction primitive with narrower, explicit tools.
Subatomic’s thesis is simple: remove ambiguity from transaction boundaries, even if that makes the API less convenient.
Key Takeaways

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.

Kraken Engineering, Engineering Blog · Django Subatomic docs
A close-up of a hand trying to drag a savepoint tool across a function boundary, but a guardrail stops it. Inside the boxed region the tool works as a local rollback pocket, while outside it is rejected as a decorator-like misuse. The image explains why the library forbids ambiguous transaction helpers from leaking across scope.
The constraint is the point. Some helpers are allowed only where their scope is obvious.

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.

Subatomic’s value becomes clearest when you watch the same function move through real transactions, nested calls, and test wrappers.

BehaviorDjango `atomic()`Subatomic primitive
Start a new real transactionMay create a transaction or savepoint depending on context`transaction()` must start a real boundary or fail
Require an existing transactionNot expressed directly`transaction_required()` makes the requirement explicit
Create a local rollback pocketImplicit when nested`savepoint()` exists for that exact job
Allow nesting by defaultYesNo for `transaction()`, by design
Read well in testsCan be misleading under `TestCase`Checks are shaped to expose test-suite illusions
Misuse as decoratorCommon and acceptedSome 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()
PrimitiveMeaningDesign signal
`transaction()`Start a durable boundary or failOwnership is explicit
`transaction_required()`Run only if a real transaction already existsDependency is explicit
`savepoint()`Create a local rollback pocketScope stays local
`transaction_if_not_already()`Enter only if neededThis is an exception path, not a default
`part_of_a_transaction`Test helper for transaction-shaped codeTests 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.

Kraken Engineering, Engineering Blog · Static typing Python at scale

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 goalDjango `atomic()`Subatomic
Primary valueConvenience and flexibilityExplicitness and correctness
Developer burdenLow upfront frictionHigher upfront precision
Failure modeAmbiguous nesting and misleading testsEarlier errors and fewer hidden assumptions
Mental modelOne helper that adaptsSeveral helpers that state intent
Scaling postureFramework magic toleratedFramework 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.