Django

Lesson 09

Transactions and side effects

Wrap related database writes in atomic transactions and run external side effects after commit.

Good Code

reviews/services.py
from django.db import transaction


def approve_review(review_id, moderator):
    with transaction.atomic():
        review = Review.objects.select_for_update().get(pk=review_id)
        review.approve(moderator=moderator)
        review.save(update_fields=["status", "approved_by", "approved_at"])

        AuditLog.objects.create(
            actor=moderator,
            action="review.approved",
            review=review,
        )

        transaction.on_commit(
            lambda: send_review_approved_email(review.pk)
        )

    return review

Bad Code

reviews/services.py
def approve_review(review_id, moderator):
    review = Review.objects.get(pk=review_id)
    review.approve(moderator=moderator)
    review.save()

    send_review_approved_email(review.pk)

    AuditLog.objects.create(
        actor=moderator,
        action="review.approved",
        review=review,
    )
    return review

Review Notes

What to review

Good Code

The good version locks the row, saves the review and audit log atomically, and sends email only after the database commit succeeds.

Bad Code

The bad version can send an email before the audit row is written. If the final write fails, the outside world has already been told the change happened.

Takeaways

  • Database changes and external notifications need an explicit ordering contract.