Prompt
How should email infrastructure fit into a modern web stack?
Latest observation
Email infrastructure should be treated as a core platform service, not as an afterthought bolted onto the app. In a modern web stack, it usually sits alongside your database, cache, object storage, and background jobs—because email is asynchronous, stateful, and operationally important.
1) Treat email as an event-driven subsystem
Most application email should be triggered by events:
- user signed up
- password reset requested
- order shipped
- invoice failed
- weekly digest generated
A good architecture is:
Web app → emits event → queue/job system → email service/provider → delivery tracking
This keeps your request/response path fast and avoids sending mail inside the user-facing request.
2) Separate transactional and marketing email
They have different requirements:
- Transactional email
- password resets, receipts, alerts
- must be fast, reliable, and highly deliverable
- usually sent from application-owned infrastructure or a specialized provider
- Marketing email
- newsletters, campaigns, promotions
- needs list management, segmentation, unsubscribe compliance, analytics
- often belongs in a dedicated marketing platform
Mixing them can hurt deliverability and compliance.
3) Use a dedicated email provider unless you have a strong reason not to
For most teams, infrastructure should rely on a provider such as:
- SES
- SendGrid
- Postmark
- Mailgun
- Resend
These handle:
- SMTP/API delivery
- retries
- bounce and complaint handling
- deliverability reputation
- domain authentication support
Self-hosting mail servers is usually not worth it for application email unless email itself is your product or you have strict requirements.
4) Make email sending asynchronous and idempotent
Email jobs should be queued and retryable.
Important practices:
- use a job queue/background worker
- retry on transient failures
- ensure idempotency so the same event doesn’t send duplicate mail
- store message IDs and send status in your database
- have dead-letter handling for persistent failures
5) Design for deliverability from day one
Email infrastructure should include:
- SPF
- DKIM
- DMARC
- dedicated sending domain
- proper reply-to and from-address strategy
- bounce/complaint webhooks
- list hygiene and suppression management
If you don’t set this up early, your app emails may land in spam or fail entirely.
6) Build observability around email
You should be able to answer:
- Was the email queued?
- Was it accepted by the provider?
- Did it bounce?
- Was it complained about?
- Did the user open/click it?
(If relevant and privacy-compliant)
Log and monitor:
- send attempts
- success/failure rates
- provider latency
- bounce/complaint rates
- template rendering errors
7) Keep templates and content versioned
Email content should be managed like application code:
- templates in source control
- reusable components/partials
- preview/staging environment
- localization support if needed
- versioning for critical emails
This reduces accidental regressions in important flows like password reset or checkout.
8) Support multi-environment and multi-tenant concerns
Modern stacks should isolate:
- dev/staging/prod sending
- test recipients vs real users
- tenant-specific branding or domains, if SaaS
- per-tenant rate limits or suppression lists if needed
A test environment should never accidentally send real mail.
9) Consider compliance and user preferences
Depending on your use case, you may need:
- unsubscribe links
- consent tracking
- region-specific compliance controls
- retention policies for email logs
- privacy-conscious analytics
10) Practical placement in the stack
A common setup looks like this:
- Frontend
- triggers user actions
- Backend API
- validates request and emits domain event
- Queue / worker
- renders template and sends email
- Email provider
- handles transport and delivery
- Webhook handler
- receives bounce/complaint/delivery events
- Database
- stores send state, suppression lists, audit logs
Rule of thumb
If email matters to your product, it should have:
- its own abstraction
- its own queue/worker path
- its own monitoring
- its own deliverability setup
- its own test strategy
If you want, I can also sketch a reference architecture diagram or recommend a setup for a specific stack like Next.js + Node, Rails, or Django.