Typorithm · medium

A multi-step write needs an atomic storage boundary

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

Step 1 of 4 · intro card

A multi-step write needs an atomic storage boundary

0%

Concept and contract

Validate first, catch integrity failures outside atomic, and observe rollback.

Two individually valid writes may need to succeed or fail together. Updating one role and then creating another should not leave the first change committed when a database uniqueness constraint rejects the second. A serializer checks request values, while a transaction controls the storage effect.

The fixture validates two titles and a small slots count. Inside transaction.atomic it renames an owned row and creates another under the same owner. A real unique owner/title constraint can reject a duplicate second title, allowing the test to observe whether the first change rolled back.

Mechanism and worked example

Validation runs before opening the atomic block.

Validation runs before opening the atomic block. IntegrityError is caught outside it, after Django has had the opportunity to roll back the failed transaction. The helper then reads actual stored titles and the created slots value rather than trusting an optimistic response label.

The owned database resets between cases. A successful operation produces both intended changes; a duplicate second title leaves the original four fixture rows and original first title intact. The helper never opens the ApplyDjinn database or contacts a feed.

Complete worked example

python

from drf_lab import setup
setup()

from contextlib import nullcontext
from django.db import IntegrityError, transaction
from rest_framework import serializers
from drf_lab import reset_fixture
from drf_lab.models import Role

class Input(serializers.Serializer):
    first = serializers.CharField(max_length=80)
    second = serializers.CharField(max_length=80)
    slots = serializers.IntegerField(min_value=1, max_value=2, default=1)

def inspect_example(payload):
    alice, _, company = reset_fixture()
    serializer = Input(data=payload)
    if not serializer.is_valid():
        return {"error": "validation", "titles": list(Role.objects.filter(owner=alice).order_by("id").values_list("title", flat=True))}
    values = serializer.validated_data
    created = None
    try:
        with transaction.atomic():
            Role.objects.filter(pk=1, owner=alice).update(title=values["first"])
            created = Role.objects.create(owner=alice, company=company, country="UA",
                                          title=values["second"], slots=values["slots"])
        committed = True
    except IntegrityError:
        committed = False
    return {"committed": committed, "titles": list(Role.objects.filter(owner=alice).order_by("id").values_list("title", flat=True)),
            "new_slots": created.slots if committed else None}
Reasoning
Validation runs before opening the atomic block. IntegrityError is caught outside it, after Django has had the opportunity to roll back the failed transaction. The helper then reads actual stored titles and the created slots value rather than trusting an optimistic response label. The owned database resets between cases. A successful operation produces both intended changes; a duplicate second title leaves the original four fixture rows and original first title intact. The helper never opens the ApplyDjinn database or contacts a feed.

Focused decision: context is in the complete example

python

try:
    with transaction.atomic():
        Role.objects.filter(pk=1, owner=alice).update(title=values["first"])
        created = Role.objects.create(owner=alice, company=company,
                                      country="UA", title=values["second"])
except IntegrityError:
    committed = False
Focused decision
Reconstruct this focused decision after the warm-up. The complete imports, model or application, and test client remain in the worked example. Typing records practice; only the downloadable computer lab executes Python.

Counterexamples and common mistakes

The counterexample replaces atomic with a no-op context manager.

The counterexample replaces atomic with a no-op context manager. When the second write fails, the earlier rename remains committed under autocommit. An error response alone therefore does not prove that no partial change happened.

Do not catch an integrity error inside the same atomic block and continue querying as though the connection were healthy. Also avoid claiming SQLite fixtures prove every production isolation behavior. Row locking, deadlock handling and concurrent uniqueness races depend on the actual storage backend and service policy.

Controlled mistake: observe the actual wrong behavior

python

from drf_lab import setup
setup()

from contextlib import nullcontext
from django.db import IntegrityError, transaction
from rest_framework import serializers
from drf_lab import reset_fixture
from drf_lab.models import Role

class Input(serializers.Serializer):
    first = serializers.CharField(max_length=80)
    second = serializers.CharField(max_length=80)
    slots = serializers.IntegerField(min_value=1, max_value=2, default=1)

def inspect_example(payload):
    alice, _, company = reset_fixture()
    serializer = Input(data=payload)
    if not serializer.is_valid():
        return {"error": "validation", "titles": list(Role.objects.filter(owner=alice).order_by("id").values_list("title", flat=True))}
    values = serializer.validated_data
    created = None
    try:
        with nullcontext():
            Role.objects.filter(pk=1, owner=alice).update(title=values["first"])
            created = Role.objects.create(owner=alice, company=company, country="UA",
                                          title=values["second"], slots=values["slots"])
        committed = True
    except IntegrityError:
        committed = False
    return {"committed": committed, "titles": list(Role.objects.filter(owner=alice).order_by("id").values_list("title", flat=True)),
            "new_slots": created.slots if committed else None}
Reasoning
The counterexample replaces atomic with a no-op context manager. When the second write fails, the earlier rename remains committed under autocommit. An error response alone therefore does not prove that no partial change happened. Do not catch an integrity error inside the same atomic block and continue querying as though the connection were healthy. Also avoid claiming SQLite fixtures prove every production isolation behavior. Row locking, deadlock handling and concurrent uniqueness races depend on the actual storage backend and service policy.

Independent practice

Increase the accepted slots maximum from two to three while preserving the transaction boundary and rollback behavior.

Increase the accepted slots maximum from two to three while preserving the transaction boundary and rollback behavior. A three-slot creation should succeed; four should fail request validation before either write. A duplicate title must still roll back the preceding rename.

Run those three cases and inspect stored state. Do not remove the unique constraint or swallow the second failure to make the task pass. The small request-bound change should not alter the all-or-nothing storage contract.

Step 1: Accept up to three slots while retaining pre-write validation and complete rollback on a duplicate title. Step 2: Retain the working imports and application structure. Change the stated policy, run the supplied practice cases, then explain the newly accepted or rejected boundary.

Practice starter: solve before reading the solution

python

from drf_lab import setup
setup()

from contextlib import nullcontext
from django.db import IntegrityError, transaction
from rest_framework import serializers
from drf_lab import reset_fixture
from drf_lab.models import Role

class Input(serializers.Serializer):
    first = serializers.CharField(max_length=80)
    second = serializers.CharField(max_length=80)
    slots = serializers.IntegerField(min_value=1, max_value=2, default=1)

def inspect_example(payload):
    alice, _, company = reset_fixture()
    serializer = Input(data=payload)
    if not serializer.is_valid():
        return {"error": "validation", "titles": list(Role.objects.filter(owner=alice).order_by("id").values_list("title", flat=True))}
    values = serializer.validated_data
    created = None
    try:
        with transaction.atomic():
            Role.objects.filter(pk=1, owner=alice).update(title=values["first"])
            created = Role.objects.create(owner=alice, company=company, country="UA",
                                          title=values["second"], slots=values["slots"])
        committed = True
    except IntegrityError:
        committed = False
    return {"committed": committed, "titles": list(Role.objects.filter(owner=alice).order_by("id").values_list("title", flat=True)),
            "new_slots": created.slots if committed else None}

# Independent task: Accept up to three slots while retaining pre-write validation and complete rollback on a duplicate title.
Reasoning
Accept up to three slots while retaining pre-write validation and complete rollback on a duplicate title.
  • Hint 1: Find the one field or branch that owns this policy.
  • Hint 2: Compare the first changed acceptance case with the retained baseline.

Solution, tests and trade-offs

The solution changes max_value only.

The solution changes max_value only. Valid data still crosses the same atomic boundary, and the final readback proves that both writes committed together or neither did. Rejected request values do not enter the write block.

For nested serializer writes, implement an explicit create/update policy instead of assuming ModelSerializer chooses how every related object should be merged. Transactional storage also does not make an external email or queue publish atomic automatically; those integrations need a deliberate post-commit or durable outbox strategy.

Practice solution and acceptance checks

python

from drf_lab import setup
setup()

from contextlib import nullcontext
from django.db import IntegrityError, transaction
from rest_framework import serializers
from drf_lab import reset_fixture
from drf_lab.models import Role

class Input(serializers.Serializer):
    first = serializers.CharField(max_length=80)
    second = serializers.CharField(max_length=80)
    slots = serializers.IntegerField(min_value=1, max_value=3, default=1)

def inspect_example(payload):
    alice, _, company = reset_fixture()
    serializer = Input(data=payload)
    if not serializer.is_valid():
        return {"error": "validation", "titles": list(Role.objects.filter(owner=alice).order_by("id").values_list("title", flat=True))}
    values = serializer.validated_data
    created = None
    try:
        with transaction.atomic():
            Role.objects.filter(pk=1, owner=alice).update(title=values["first"])
            created = Role.objects.create(owner=alice, company=company, country="UA",
                                          title=values["second"], slots=values["slots"])
        committed = True
    except IntegrityError:
        committed = False
    return {"committed": committed, "titles": list(Role.objects.filter(owner=alice).order_by("id").values_list("title", flat=True)),
            "new_slots": created.slots if committed else None}
Reasoning
The solution changes max_value only. Valid data still crosses the same atomic boundary, and the final readback proves that both writes committed together or neither did. Rejected request values do not enter the write block. For nested serializer writes, implement an explicit create/update policy instead of assuming ModelSerializer chooses how every related object should be merged. Transactional storage also does not make an external email or queue publish atomic automatically; those integrations need a deliberate post-commit or durable outbox strategy.

Interview reasoning and next step

Explain the difference between validation failure and a database constraint failure that occurs after a first write.

Explain the difference between validation failure and a database constraint failure that occurs after a first write. Identify why reading the database after the failed request is essential evidence. Then describe why the exception is caught outside atomic.

Next, apply a bounded request quota using a deterministic local clock. Quotas control service usage; they do not provide the storage guarantees of a transaction or a complete perimeter against hostile traffic.

Optional stretch — after the core task works: Add one independently reasoned boundary case and explain its expected value before running it.

Computer practice: https://applydjinn.com/typorithm/drf-lab-20261105.zip. Install pinned dependencies in an isolated Python environment. Run verify.py, then change the working exercise baseline and run --practice drf-transactions. The real DRF clients, serializers, schemas and SQLite transactions use owned fixtures only. No production database, feed, account or paid service is used. This lab executes Python; the PC typing trainer and native reading progress do not execute it.

Validate first, catch integrity failures outside atomic, and observe rollback.