SendDart in Django: Put Notification Intent Inside the Transaction
Design Django email delivery around transaction boundaries, durable outbox records and Python SDK exception handling rather than fragile model side effects.
TL;DR
- Treat notification intent as durable business data: record the message with the committing event so workers can recover if a web process dies between DB change and queue publication.
- Use an in-transaction outbox plus minimal task args and on_commit signaling: store an outbox row in the same transaction, pass a notification identifier, and let workers load and verify state.
- Recognize limits and verify with tests: transaction.on_commit alone isn't durable across process failure, so test rollback, lost signaling, duplicate execution, and SDK failure modes.
A saved model does not always mean a committed event
Django applications often send email near model creation, but the surrounding transaction may still roll back. A welcome message sent before commit can describe an account that never becomes durable. Sending after commit avoids that particular problem, yet a separate queue publication can still fail after the database change succeeds.
Start by deciding which business event authorizes the notification and where that event commits. Keep the intended message as a durable record associated with the event. This gives the worker something to recover even if a web process stops between the database operation and queue publication.
This guide is an architectural review for Django applications. It does not provide an untested code snippet or claim that one framework hook alone guarantees delivery.
Use transaction hooks for their actual purpose
Django documents transaction.on_commit for callbacks that should run after a successful commit. That is useful when coordinating work that must not happen after a rollback. It does not, by itself, make an external network call durable across a process failure. Django transaction documentation.
For important notifications, store an outbox row in the same transaction as the business event. An on-commit callback can signal a worker as an optimization, while periodic or queue-driven processing can still discover retained work if the signal is lost.
Avoid hiding essential sending behavior in a model signal without understanding every path that triggers it. Imports, administrative edits and tests may save the model for reasons unrelated to a new customer notification. A named service operation makes the authorization and purpose easier to review.
Keep task arguments small and stable
Pass a notification identifier to the background task rather than a large mutable model object or live provider client. The worker can load the intended version and current eligibility under the correct tenant scope.
Decide which data is frozen and which policy is rechecked. The content associated with an order receipt should remain tied to the original event snapshot. Suppression and sender authorization should be checked near execution because they may have changed since the job was created.
Do not let a retry silently render a newer template under the original provider operation identity. Store the template version or rendered content according to your data-retention policy. If a new message is needed, create a distinct authorized notification.
Handle the Python SDK's exception model
The SendDart Python SDK returns parsed response data on success and raises SendDartError for documented non-success API responses. Handle the structured exception fields and retain recovery evidence. Do not copy the Node.js data/error pattern into Python. SendDart Python SDK.
Translate failures into task outcomes deliberately. A template validation problem should not be retried indefinitely. A rate rejection may be delayed under a bounded policy. An interrupted request may be uncertain and require reconciliation rather than a fresh send.
Configure the SDK from trusted server settings. Its documented module-level configuration deserves care when different tenants use different credentials. Do not mutate a shared global key concurrently and assume task isolation will happen automatically.
Prevent duplicate tasks from becoming duplicate mail
Task systems can repeat work. Enforce a business uniqueness rule for the event and message purpose, and claim notification execution atomically. A task arriving twice should find the same record rather than create two independent operation keys.
For supported SendDart send operations, retain a stable idempotency key and unchanged payload during recovery. Keep provider identifiers with the notification. A crash after acceptance but before the local update should leave enough evidence for the next worker to recover the original operation.
Test two workers competing for one notification. Sequential tests alone can miss a race in the database claim. Also test a worker whose lease expires after submission; an expired lease is not proof that the message was never accepted.
Related reading: Best Email API: Choose With a Production Acceptance Test.
Build the webhook as a separate trust boundary
Preserve the raw request body required by the documented signature verifier. Authenticate the callback before applying its contents to customer or notification records. Use bounded request sizes and validate the event schema after verification.
Store a unique event observation and process side effects idempotently. A delivery event should update evidence; a complaint may affect future eligibility under your policy. Neither should create a new business notification merely because the callback was replayed.
Scope correlation through the stored integration and message ownership. Avoid trusting a caller-supplied tenant identifier as authorization. Events that arrive before the send result is committed need a defined pending-correlation path.
Related reading: Scheduled Email Delivery: Store the Time, Content and Cancellation State.
Test transaction and request boundaries
Write tests for rollback, successful commit, lost queue signaling and duplicate task execution. Confirm that rollback creates no sendable intent and that retained outbox work survives a failed signal.
In Django tests, understand the transaction behavior of the test class and how on-commit callbacks are exercised. A test that never commits can produce misleading results if it assumes a callback ran. Follow the framework's documented testing facilities for the version you use.
Add controlled SDK simulations for rejection and uncertainty, then a separate provider canary for credentials and domain setup. Keep real customer addresses out of fixtures and logs.
A dependable Django integration makes the transaction boundary visible. Business state and notification intent commit together, workers own external execution, and provider events add evidence without becoming hidden model side effects.