Typorithm · medium

Atomic writes and on-commit work

pythondjangoindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Atomic writes and on-commit work

0%

Concept and contract

Keep a multi-step write all-or-nothing and schedule follow-up work only after a successful commit.

A successful operation can require several writes that must become visible together. transaction.atomic supplies a database transaction boundary, while on_commit registers follow-up work that should run only after that boundary succeeds.

This lesson records a fictional publication event in a local list. It does not send email, publish a real message or call an external service, so both success and rollback can be tested without side effects outside the fixture.

Mechanism and worked example

The helper creates a role inside atomic, registers the callback and optionally raises a controlled error before leaving the block.

The helper creates a role inside atomic, registers the callback and optionally raises a controlled error before leaving the block. On success the row remains and the callback runs after commit.

On failure the row is rolled back and the registered callback is discarded. The result observes both database state and the event list, making a false success visible even when one of those two observations alone looks plausible.

Complete worked example

python

from bootstrap_lab import django_setup
django_setup(__name__)

from bootstrap_lab import install_models
from django.db import models, connection, transaction
from django.test.utils import CaptureQueriesContext

class Role(models.Model):
    title = models.CharField(max_length=80)
    country = models.CharField(max_length=2)
    active = models.BooleanField(default=True)
    class Meta:
        app_label = "framework_lab"

install_models(Role)

def publish_after_commit(fail):
    Role.objects.all().delete()
    events = []
    try:
        with transaction.atomic():
            Role.objects.create(title="Saved", country="UA", active=True)
            transaction.on_commit(lambda: events.append("published"))
            if fail:
                raise ValueError("controlled rollback")
    except ValueError:
        pass
    return {"rows": Role.objects.count(), "events": events}
Reasoning
The helper creates a role inside atomic, registers the callback and optionally raises a controlled error before leaving the block. On success the row remains and the callback runs after commit.

Counterexamples and common mistakes

The counterexample appends the event immediately instead of registering on_commit.

The counterexample appends the event immediately instead of registering on_commit. A failed operation then advertises publication despite having no committed role.

Another mistake is catching a database error inside an atomic block and continuing to query a broken transaction. A callback is also not a durable queue: process failure after commit can still prevent later work, so critical delivery may need a transactional outbox and a separate worker.

Counterexample: a runnable framework mistake

python

from bootstrap_lab import django_setup
django_setup(__name__)

from bootstrap_lab import install_models
from django.db import models, connection, transaction
from django.test.utils import CaptureQueriesContext

class Role(models.Model):
    title = models.CharField(max_length=80)
    country = models.CharField(max_length=2)
    active = models.BooleanField(default=True)
    class Meta:
        app_label = "framework_lab"

install_models(Role)

def publish_after_commit(fail):
    Role.objects.all().delete()
    events = []
    try:
        with transaction.atomic():
            Role.objects.create(title="Saved", country="UA", active=True)
            events.append("published")
            if fail:
                raise ValueError("controlled rollback")
    except ValueError:
        pass
    return {"rows": Role.objects.count(), "events": events}
Reasoning
The counterexample appends the event immediately instead of registering on_commit. A failed operation then advertises publication despite having no committed role.

Independent practice: one bounded framework change

Create a second fictional role in the same atomic block.

Create a second fictional role in the same atomic block. Preserve the single publication callback and the rollback behavior.

Begin with success, which should retain both rows and one event, then inject the same failure and require neither row nor event. This changes the size of the owned batch without introducing a broker, a background task runner or an unbounded retry policy.

Step 1: Create two rows inside the existing atomic block. Step 2: Keep one callback and preserve complete rollback.

Practice starter: solve before reading the solution

python

from bootstrap_lab import django_setup
django_setup(__name__)

from bootstrap_lab import install_models
from django.db import models, connection, transaction
from django.test.utils import CaptureQueriesContext

class Role(models.Model):
    title = models.CharField(max_length=80)
    country = models.CharField(max_length=2)
    active = models.BooleanField(default=True)
    class Meta:
        app_label = "framework_lab"

install_models(Role)

def publish_after_commit(fail):
    # Remove this placeholder, then adapt the retained baseline:
    # Create two rows inside the existing atomic block.
    raise NotImplementedError
    Role.objects.all().delete()
    events = []
    try:
        with transaction.atomic():
            Role.objects.create(title="Saved", country="UA", active=True)
            transaction.on_commit(lambda: events.append("published"))
            if fail:
                raise ValueError("controlled rollback")
    except ValueError:
        pass
    return {"rows": Role.objects.count(), "events": events}
Reasoning
Create two rows inside the existing atomic block. Keep one callback and preserve complete rollback.
  • Hint 1: Add the second insert before the controlled failure point.
  • Hint 2: Check both row count and the event list.

Solution, tests and trade-offs

The expected results distinguish successful persistence from post-commit follow-up evidence.

The expected results distinguish successful persistence from post-commit follow-up evidence. The practice checks that two rows disappear together when the later exception is raised.

Because every call starts from a controlled empty role table, a stale earlier row cannot accidentally make a failed batch appear successful. These checks run on the pinned SQLite fixture; production isolation levels, concurrent writers and external delivery need their own evidence.

Practice solution and acceptance checks

python

from bootstrap_lab import django_setup
django_setup(__name__)

from bootstrap_lab import install_models
from django.db import models, connection, transaction
from django.test.utils import CaptureQueriesContext

class Role(models.Model):
    title = models.CharField(max_length=80)
    country = models.CharField(max_length=2)
    active = models.BooleanField(default=True)
    class Meta:
        app_label = "framework_lab"

install_models(Role)

def publish_after_commit(fail):
    Role.objects.all().delete()
    events = []
    try:
        with transaction.atomic():
            Role.objects.create(title="Saved", country="UA", active=True)
            Role.objects.create(title="Second", country="UA", active=True)
            transaction.on_commit(lambda: events.append("published"))
            if fail:
                raise ValueError("controlled rollback")
    except ValueError:
        pass
    return {"rows": Role.objects.count(), "events": events}
Reasoning
The expected results distinguish successful persistence from post-commit follow-up evidence. The practice checks that two rows disappear together when the later exception is raised.

Interview reasoning and next step

Explain the points at which the database commits and the callback becomes eligible.

Explain the points at which the database commits and the callback becomes eligible. Nested atomic blocks may introduce savepoints, but leaving an inner block is not necessarily the outermost commit.

A callback failure does not reverse an already committed transaction. Keep these facts separate from Django’s newer task APIs and the backend that actually executes work. The migration lesson next addresses changes to schema and historical model state rather than ordinary application rows.

Optional stretch — after the core task works: Design an outbox fixture that distinguishes committed intent from external delivery.

Computer practice: https://applydjinn.com/typorithm/python-frameworks-lab-20261105.zip. Install the pinned dependencies in an isolated environment, run verify.py --track django, then edit the retained exercise baseline and check it with --practice dj-transactions. The lab uses owned in-memory databases, local framework clients and tiny CPU tensors. It never reads the working ApplyDjinn database, contacts production feeds or downloads models.

Keep a multi-step write all-or-nothing and schedule follow-up work only after a successful commit.