Prompt
How should my backend trigger transactional emails?
Latest observation
The best pattern is: your backend should trigger transactional emails by emitting an email job/event, not by sending the email inline in the request path.
Recommended flow
-
User action happens
- e.g. sign up, password reset, order placed.
-
Backend creates/updates state in your DB
- e.g. user record, reset token, order status.
-
Backend writes an email intent
- either:
- enqueue a job in a queue, or
- publish an event to a message bus, or
- store an
outboxrecord in the same transaction.
- either:
-
A separate email worker/service sends the email
- calls your email provider
- handles retries, rate limits, failures, and idempotency
Why this is the right approach
1. Keeps your user request fast
Sending email synchronously can slow down the request or fail the whole action if the email provider has issues.
2. Makes email delivery reliable
If the DB write succeeds but the email send fails, you can retry safely.
3. Avoids duplicate sends
A queue + idempotency key or outbox pattern helps prevent accidental double emails.
4. Scales better
Email sending can be bursty; a worker can process messages at its own pace.
Best practice: Outbox pattern
If the email should only be sent when the database transaction succeeds, the outbox pattern is ideal:
- In the same DB transaction that creates the business record, insert an
outboxrow like:- type:
send_email - template:
welcome_email - recipient: user email
- payload:
{ userId, name }
- type:
- A worker polls the outbox table or streams changes and sends the email.
- Once sent, mark the outbox row as processed.
This avoids the classic failure mode:
- DB commit succeeds
- email enqueue fails
because the intent to send the email is stored atomically with your business data.
When to send synchronously
Only consider inline sending if:
- the email is truly optional,
- the action is low-volume,
- and you can tolerate occasional delays/failures.
Even then, it’s usually better to queue it.
Practical implementation options
Option A: Queue
Use Redis/RabbitMQ/SQS/Sidekiq/Celery/BullMQ, etc.
- API handler enqueues:
send_welcome_email(userId)
- Worker processes queue and calls email provider.
Option B: Outbox + worker
Best for consistency when email depends on DB state.
Option C: Event-driven
Emit events like:
user.createdpassword.reset.requestedorder.paid
Then a mailer service subscribes and sends the right template.
Things to include in your email job
- recipient address
- template name
- template variables
- unique idempotency key
- locale/timezone if relevant
- correlation/user/order ID for tracing
Safety/quality tips
- Make sends idempotent
- Retry transient failures
- Log provider response IDs
- Track delivery/bounce/open events if needed
- Separate transactional emails from marketing emails
- Never block core business actions on email delivery
Simple rule of thumb
Your backend should record the intent to send transactional email, and a separate asynchronous worker should actually send it.
If you want, I can sketch a concrete implementation for your stack (e.g. Node + Postgres + BullMQ, Django + Celery, Rails + Sidekiq, etc.).