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():
# Lock the row while this approval is being saved.
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,
)
# Send email only after the transaction commits.
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()
# Email can be sent before the database work fully succeeds.
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.