Good Code
The good version locks the row, saves the review and audit log atomically, and sends email only after the database commit succeeds.
Lesson 09
Wrap related database writes in atomic transactions and run external side effects after commit.
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 reviewdef 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 reviewThe good version locks the row, saves the review and audit log atomically, and sends email only after the database commit succeeds.
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.