An outbound email is a side effect of a business event, not the business event itself. A database transaction should record the case assignment or account action and a message intent together, then a worker sends the message through a provider. This outbox boundary prevents a committed change from losing its notice when the process crashes. It also prevents a failed mail request from rolling back a legitimate case action. Delivery is not exactly once: a worker can time out after the provider accepts a message, and retries can produce duplicates unless the application uses stable intent IDs and the provider’s idempotency contract where available.
Email Intent, Outbox, and Idempotent Send
Working case
Reviewer 29 is assigned case 47. The assignment transaction writes the new owner and one email intent keyed by assignment revision 6. A worker reads that intent, checks the current recipient and purpose policy, renders the message, and asks the provider to send. The provider response times out. The worker does not create a new intent on retry; it consults the stored attempt and provider state if possible, then retries with the same key under its integration rules. A duplicate queue delivery cannot create two independent notice records. The case assignment remains valid whether mail is delayed or rejected.
Implementation boundary
function intentKey(caseId, assignmentRevision) {
return `case:${caseId}:assignment:${assignmentRevision}`;
}
console.log(intentKey(47, 6));
// Output: case:47:assignment:6Persist an intent ID, business event ID, recipient account, template key and version, purpose, locale, expiry, and status in the same transaction as the business change. Do not put a full private case note into the queue payload. A worker claims a bounded batch with lease or comparable ownership, rechecks recipient eligibility at send time, and records provider message ID and attempt outcome. Distinguish transient errors, permanent rejection, and uncertain acceptance. Use provider idempotency when supported; otherwise document the duplicate risk and make recipient-facing content tolerant of repetition. Give stale security links a short expiry instead of retrying them indefinitely. Treat retries as bounded work with backoff and a dead-letter review path.
Cost and boundaries
Writing one intent is O(1) database work; rendering and sending cost a provider call and queue capacity. Leases and retries add state but protect against process failure. An assignment wave can generate many intents, so batch worker capacity and provider rate limits must be measured. A long queue delay can make a message misleading if case ownership changes; either recheck current state or word the notice as a historical event. Track intent age, provider acceptance uncertainty, duplicate sends, permanent rejection, and user-visible latency. Queue storage grows with event rate times retention until terminal status and cleanup.
Failure trace
The application sends email before committing the assignment. The provider accepts it, then the database transaction fails; reviewer 29 receives a notice for a case never assigned. Commit the business event with an intent first. Another worker treats timeout as definite failure and sends a second message with a new key, creating duplicates. Keep one intent and reconcile uncertain attempts. Test crash before commit, crash after commit but before queue wakeup, provider timeout after acceptance, duplicate worker claim, revoked recipient access, expired link, and a message that remains queued longer than its usefulness.
Verification
- A committed business event has one durable message intent.
- Duplicate queue work does not create a new intent.
- Uncertain provider acceptance is reconciled before unsafe retry.
Practice drill
Assign case 47 to reviewer 29 and inspect the same database transaction for assignment revision 6 and one email intent. Kill the worker before sending and restart it; the notice should still be considered. Force a provider timeout after remote acceptance and exercise the chosen reconciliation rule. Deliver the queue item twice and inspect intent and provider IDs. Change the case owner while the message is queued, then verify the send-time policy produces the documented outcome without leaking private case content.
Decision note
Commit message intent with the business change, then send through a retryable worker using one stable identity.
Common Mistakes
- Sending before the business transaction commits.
- Treating a timeout as proof the provider did not accept mail.
- Putting private case notes into queue payloads.
Connected lessons
Outbound Email and Delivery State; Email Template Data and Account Link Boundaries; Email Provider Events, Bounces, and Suppression; Sender Identity, Reputation, and Message Observability; Background Jobs and the Outbox Boundary; Idempotent Write Requests and Lost Responses; Notification Opt-In and Channel Preference.
Apply and check
Build Project: account email delivery and review Web Development: file and email delivery decisions quiz.
