Prompt
What's a good transactional email architecture for a serverless app?
Latest observation
A solid transactional email architecture for a serverless app usually centers on asynchronous, event-driven email sending with a queue, a dedicated email worker, and a provider API like SES, SendGrid, Postmark, or Mailgun.
Recommended pattern
1. App emits an event
When something happens in your app—user signs up, password reset requested, payment failed—the main serverless function should not send the email inline if you can avoid it.
Instead, it should:
- write the business action
- create an
email_requestedevent or job - return quickly to the caller
2. Put the email job on a queue
Use a managed queue service such as:
- AWS SQS
- Google Cloud Pub/Sub
- Azure Queue / Service Bus
- Cloud Tasks
The queue gives you:
- retries
- backpressure handling
- decoupling from your app
- durability if the provider is slow or down
3. A separate email worker processes jobs
A serverless function or container worker consumes the queue and:
- fetches the email template
- renders personalized content
- calls the email provider API
- records send status
This worker should be idempotent so duplicate deliveries don’t send duplicate emails.
4. Use a transactional email provider
For most serverless apps, the provider should handle:
- SMTP/API delivery
- bounce and complaint webhooks
- suppression lists
- delivery tracking
- templates, if you choose to use them
Common choices:
- AWS SES: cheapest, solid if you’re on AWS, more setup
- Postmark: excellent for transactional email, very reliable
- SendGrid: popular, feature-rich
- Mailgun: flexible, good API
5. Handle provider webhooks
Set up webhook endpoints for:
- bounces
- complaints
- deliveries
- deferrals
Store these events in your database or analytics pipeline so you can:
- suppress bad addresses
- monitor deliverability
- debug failures
Good reference architecture
Simple version
- API Gateway / app frontend
- Serverless function writes business data
- Same function writes email job to SQS
- Lambda worker consumes SQS
- Worker sends via SES/Postmark
- Webhook endpoint receives bounces/complaints
More robust version
- App service emits domain events
- Event bus (SNS, EventBridge, Pub/Sub)
- Email service subscribes to
user.signed_up,order.paid, etc. - Email service writes jobs to queue
- Worker renders and sends
- Dedicated webhook handler updates suppression/status tables
- Metrics and alerting in CloudWatch / Datadog / Stackdriver
Key design principles
Keep business logic and email delivery separate
Your app should decide that an email needs to be sent, but not worry about delivery details.
Make sending idempotent
Use an idempotency key like:
userId + emailType + eventId
That prevents duplicates if retries happen.
Prefer queue-based retries over inline retries
If the email API fails temporarily, the queue can retry with exponential backoff.
Store template versions
If you update an email template, know which version was used for a given send.
Separate “command” from “notification”
Sometimes it helps to have:
- a domain event or command triggers
- a notification service transforms it into email payloads
This keeps the system maintainable as email types grow.
Template strategy
You have three common options:
Provider-hosted templates
Pros:
- easy
- fast to launch
Cons:
- harder to version alongside code
- less flexible for complex rendering
App-owned templates
Store templates in code, files, or a template service.
Pros:
- versioned with deployment
- easier testing
- more control
Cons:
- you manage rendering and previews
Hybrid
- Use app-owned templates for layout and logic
- Use provider templates for branding blocks or simple variants
For serverless apps, the hybrid or app-owned approach is often best long term.
Operational concerns
Observability
Track:
- enqueue count
- send success/failure rate
- provider latency
- retry count
- bounce/complaint rate
Dead-letter queue
Have a DLQ for jobs that fail repeatedly so you can inspect and replay them.
Rate limiting
Protect against bursts and provider limits:
- control worker concurrency
- batch or throttle sends if needed
Compliance
Support:
- unsubscribe links where relevant
- suppression lists
- regional data handling if needed
- proper sender identity and DKIM/SPF/DMARC setup
If you want the simplest possible setup
If your volume is low and reliability requirements are modest:
- serverless function calls email provider directly
- still use an idempotency key
- log send status
- add webhook handling
This is simpler, but once you care about retries, burst handling, or resilience, move to the queue-based pattern.
My default recommendation
For most serverless apps:
API/app -> queue -> email worker -> provider API -> webhook processor
That gives the best balance of:
- reliability
- scalability
- cost
- simplicity
If you want, I can also sketch this as:
- an AWS-specific architecture,
- a GCP/Azure version, or
- a concrete example with SQS + Lambda + SES/Postmark.