ADR-012: Email Delivery Strategy — Mandrill Dependency Reduction
ADR-012: Email Delivery Strategy — Mandrill Dependency Reduction
Status
Proposed — Pending engineering team review
Context
Email delivery runs through Mandrill (Mailchimp’s transactional email
API) via the lutung Java library (v0.0.8) which is
unmaintained and deprecated. This is flagged as P0 tech
debt — the library has no security updates and faces JDK compatibility
concerns. Regardless of whether Mandrill remains the provider, the
library must be replaced.
Current Email Architecture
| Component | Detail |
|---|---|
| Provider | Mandrill (Mailchimp Transactional) |
| Library | lutung v0.0.8 (deprecated, unmaintained) |
| Service | email (Java 21 / Spring Boot 3.5.4) |
| Database | Shared with SMS + Notifications
(peeq-notification-service-db) |
| Deployment | All 4 production tenants |
| API keys | Per-tenant Mandrill API keys in GCP Secret Manager |
Email Types
| Type | Trigger | Volume (estimated) |
|---|---|---|
| Order confirmations | Purchase event → RabbitMQ → email service | Per transaction |
| Shoutout notifications | Shoutout complete → RabbitMQ → email service | Per shoutout |
| Magic Link (auth) | Login request → Keycloak SPI → Mandrill | Per login session |
| Email verification | Registration → email service (DEPRECATED — migrating to Keycloak) | Per registration |
| Weekly digest | Cron (Wednesdays noon ET) → email service | Weekly per user |
| Event notifications | Event created/updated → RabbitMQ → email service | Per event |
Database Schema (email-related tables)
| Table | Purpose | Status |
|---|---|---|
user_emails |
Email verification tracking | DEPRECATED — migrating to Keycloak |
sent_messages_email |
Delivery tracking (from, to, template, mandrill_response_status) | Active |
callbacks |
Mandrill webhook data (message_id, status, JSON payload) | Active |
message_templates |
Email templates (shared with SMS) | Active |
Current Email Flow
sequenceDiagram
participant SVC as Any Service
participant RMQ as RabbitMQ
participant NOTIF as Notifications Service
participant EMAIL as Email Service
participant MANDRILL as Mandrill API
participant USER as User Inbox
SVC->>RMQ: Publish event (OrderCompleted, ShoutoutReady, etc.)
RMQ->>NOTIF: Consume event
NOTIF->>NOTIF: Check user notification preferences
NOTIF->>RMQ: Publish SendEmail {to, template, variables}
RMQ->>EMAIL: Consume SendEmail
EMAIL->>MANDRILL: Send via lutung library
MANDRILL->>USER: Deliver email
MANDRILL->>EMAIL: POST /Api/Email/Mandrill/Callback/{route} (webhook)
EMAIL->>EMAIL: Store delivery status in sent_messages_email
Problems with Current Approach
| Problem | Impact |
|---|---|
lutung library is deprecated |
No security updates, no JDK 21+ testing, no maintainer. P0 tech debt. |
| Mandrill API key per tenant | 4 separate Mandrill accounts to manage. Config drift risk. |
| No delivery analytics | sent_messages_email tracks send status but no open
rates, click rates, or bounce analysis |
| Email verification is dual-pathed | Deprecated email service API + Keycloak native. Confusing and error-prone. |
| Mandrill pricing | Per-email pricing for transactional. Costs scale linearly with user growth. |
| Template management | Templates stored in DB (message_templates table) — not
version controlled, no preview |
| Magic Link emails sent from Keycloak SPI | Auth emails bypass the email service entirely — go directly from Keycloak → Mandrill |
Rival / CortexOne Email Capability
Rival already has a production email service running on CortexOne:
| Capability | Detail |
|---|---|
| Service | email-service CortexOne function (Python) |
| Provider | Resend API (primary) |
| Fallbacks | Mailpit (dev), SMTP/Gmail (nodemailer) |
| Features | send_email, send_templated_email, validate_email, get_delivery_status |
| Security | Rate limiting (30 req/60s), injection detection (OWASP patterns), input validation |
| Templates | HTML template support with variable substitution |
| Attachments | Supported |
| CC/BCC | Supported |
| Status | Production — used for cost reports and platform notifications |
Decision
Replace Mandrill with Resend API, leveraging the existing
CortexOne email service pattern from Rival. The email service
consolidation (email + SMS + notifications → single delivery service per
ADR-001) will use Resend as the email provider, eliminating the
deprecated lutung library and Mandrill dependency.
Why Resend over Mandrill
| Criterion | Mandrill | Resend | Winner |
|---|---|---|---|
| Java SDK | lutung (deprecated) |
Official REST API + community SDKs | Resend |
| Developer experience | Mailchimp account required, complex setup | API-first, simple key-based auth | Resend |
| Deliverability | Good (Mailchimp infrastructure) | Good (built on AWS SES) | Tie |
| Pricing | Mailchimp add-on, per-email | $20/month for 50K emails, then per-email | Resend (simpler) |
| Webhooks | HMAC-signed callbacks | Webhook support with event types | Tie |
| React Email | No | Native — React Email templates | Resend |
| Already in use | Yes (Peeq platform) | Yes (Rival/CortexOne) | Resend (consolidation) |
| Template management | Mailchimp UI or API | Code-based (React Email) or API | Resend |
Why Not Other Providers
| Provider | Consideration | Verdict |
|---|---|---|
| SendGrid | Mature, good API, Twilio-owned | Viable but adds a new vendor; Resend already proven in Rival |
| Amazon SES | Cheapest at scale ($0.10/1000 emails) | Raw — no template engine, no built-in analytics. Resend is built ON SES. |
| Postmark | Excellent deliverability, great DX | Good option but more expensive; no existing usage in Rival |
| Keep Mandrill | Replace only the library | Viable for short-term but doesn’t address per-tenant API key sprawl or template management |
Target Architecture
graph TB
subgraph "Event Sources"
SVC1[Payment Service]
SVC2[Content Service]
SVC3[Shoutout Service]
PAS[Passwordless Auth Service<br/>ADR-010]
SVCN[Other Services]
end
subgraph "Delivery Service (ADR-001 consolidated)"
DS[Delivery Service<br/>email + sms + notifications merged<br/>Java 21 / Spring Boot]
PREFS[User Preferences<br/>email/sms/push opt-in]
TEMPLATES[Email Templates<br/>Version-controlled, code-based]
end
subgraph "Email Delivery"
RESEND[Resend API<br/>Primary email provider]
WH[Webhook Handler<br/>Delivery status, bounces, opens]
end
subgraph "SMS Delivery"
TWILIO[Twilio<br/>SMS provider - unchanged]
end
subgraph "Alternative: CortexOne"
CX[CortexOne Email Function<br/>Resend API<br/>For async/batch emails]
end
SVC1 -->|RabbitMQ| DS
SVC2 -->|RabbitMQ| DS
SVC3 -->|RabbitMQ| DS
PAS -->|Magic Link email| DS
SVCN -->|RabbitMQ| DS
DS --> PREFS
DS -->|Email| RESEND
DS -->|SMS| TWILIO
DS -.->|Batch/async| CX
CX -.->|Resend API| RESEND
RESEND -->|Webhook| WH
WH --> DS
Integration Approach
Option A: Direct Resend API from Java service (Recommended for MVP)
Replace lutung with direct HTTP calls to Resend API from
the consolidated delivery service. Resend’s API is simple REST — no SDK
needed.
POST https://api.resend.com/emails
Authorization: Bearer re_xxxxx
Content-Type: application/json
{
"from": "The Agile Network <noreply@theagilenetwork.com>",
"to": ["fan@example.com"],
"subject": "Your Shoutout is Ready!",
"html": "<h1>Hi {{name}}</h1><p>Your shoutout from {{expert}} is ready...</p>"
}
Option B: CortexOne email function (for batch/async)
For bulk operations (weekly digest, marketing blasts), route through the existing CortexOne email-service function. This offloads batch processing from the Java service.
Recommendation: Use Option A for transactional emails (low latency, synchronous) and Option B for batch/digest emails (async, scalable).
Magic Link Email Consolidation (ADR-010 Integration)
Currently, Magic Link auth emails are sent directly from the Keycloak SPI → Mandrill, bypassing the email service entirely. With ADR-010’s passwordless-auth-service:
| Current | Target |
|---|---|
| Keycloak SPI → Mandrill (direct) | Passwordless-auth-service → Delivery service → Resend |
| No delivery tracking for auth emails | Full delivery tracking (sent, delivered, opened, clicked) |
| Mandrill templates for auth emails | Shared templates with all platform emails |
| Separate Mandrill API key for Keycloak | Single Resend API key for all email |
This means all email — transactional, auth, digest — flows through a single delivery pipeline with consistent tracking.
Template Migration
| Current | Target |
|---|---|
Templates in message_templates DB table |
Templates in code (version-controlled) |
| No preview capability | React Email preview (development) or HTML preview endpoint |
| Mandrill template variables | Resend template variables (same pattern) |
| Per-tenant template variants | Tenant-scoped templates with brand tokens |
Migration Strategy
| Phase | Action | Risk |
|---|---|---|
| Phase 1 | Replace lutung with Resend HTTP client in email
service |
Low — swap library, keep same service |
| Phase 2 | Migrate templates from DB to code-based | Low — version control benefit |
| Phase 3 | Consolidate email + SMS + notifications into delivery service (ADR-001) | Medium — service merge |
| Phase 4 | Route Magic Link emails through delivery service (ADR-010) | Medium — auth flow change |
| Phase 5 | Set up delivery analytics (open rates, click rates, bounce handling) | Low — Resend provides this |
| Phase 6 | Decommission Mandrill accounts | Low — after all traffic migrated |
Cost Comparison
| Mandrill | Resend | |
|---|---|---|
| Base cost | Mailchimp plan required + transactional add-on | $20/month (50K emails included) |
| Per email | Varies by Mailchimp plan | $0.00028/email after 50K |
| Webhooks | Included | Included |
| Dedicated IP | Add-on | Available at higher tiers |
| Templates | Mailchimp UI | API or React Email |
| Multi-tenant | Separate accounts/API keys per tenant | Single account, sender domains per tenant |
For a platform with 4 tenants and moderate email volume, Resend at $20/month is likely cheaper than 4 separate Mandrill/Mailchimp accounts.
Hypothesis Background
Primary: Replacing Mandrill with Resend API
eliminates the deprecated lutung library (P0 tech debt),
consolidates email delivery under a single provider already proven in
the Rival platform, and enables unified delivery tracking across all
email types including Magic Link authentication.
- Evidence:
lutungv0.0.8 is unmaintained — flagged as P0 tech debt blocking migration. - Evidence: CortexOne email-service on Rival already uses Resend API in production with rate limiting, injection detection, and delivery tracking.
- Evidence: Magic Link auth emails currently bypass the email service (Keycloak SPI → Mandrill directly). Consolidating to a single delivery pipeline gives visibility into auth email delivery.
- Evidence: 4 separate Mandrill API keys (one per tenant) adds operational overhead. Resend supports multi-domain sending from a single account.
Alternative 1: Replace lutung but keep Mandrill. - Viable for short-term fix. Replace deprecated library with direct Mandrill REST API calls. Doesn’t address per-tenant API key sprawl or template management, but eliminates the P0 tech debt with minimal change.
Alternative 2: Switch to SendGrid. - Viable. SendGrid has excellent Java SDK and deliverability. However, introduces a new vendor relationship when Resend is already proven in the Rival ecosystem.
Alternative 3: Switch to Amazon SES directly. - Cheapest at scale but requires building template engine, bounce handling, and analytics from scratch. Resend is built on SES and adds these features.
Alternative 4: Use CortexOne email function as the primary delivery path. - Partially adopted — recommended for batch/digest emails. Not recommended for transactional emails where Java service latency matters (synchronous sends).
Falsifiability Criteria
- If Resend deliverability is measurably worse than Mandrill (>5% difference in inbox placement) → investigate domain reputation or switch to SendGrid
- If Resend API latency adds >500ms to transactional email sends → evaluate direct SES or keep Mandrill with new library
- If Resend pricing exceeds Mandrill at the platform’s actual volume → re-evaluate provider choice
- If migrating Magic Link emails from Keycloak SPI → delivery service increases auth email delivery failure rate → keep direct-send path for auth
- If CortexOne email function cannot handle weekly digest volume reliably → use Java service for all email types
Evidence Quality
| Evidence | Assurance |
|---|---|
lutung library is deprecated/unmaintained |
L2 (verified — P0 tech debt inventory) |
| Mandrill is current email provider | L2 (verified — all 4 tenants) |
| CortexOne email-service uses Resend | L2 (verified — Rival production) |
| Email verification migrating to Keycloak | L1 (ADR-010, in progress) |
| Magic Link emails bypass email service | L1 (Keycloak SPI sends directly to Mandrill) |
| Resend deliverability vs Mandrill | L0 (not benchmarked) |
| Actual Mandrill monthly costs | L0 (no billing data available) |
| Monthly email volume | L0 (not measured) |
| Template count for migration | L0 (message_templates table not inventoried) |
Overall: L1 (WLNK capped by deliverability comparison L0 and cost data L0)
Bounded Validity
- Scope: All email delivery across all tenants — transactional, auth (Magic Link), and digest. Affects email service, notifications service, and passwordless-auth-service (ADR-010).
- Expiry: Re-evaluate if email volume exceeds 500K/month (Resend pricing tiers change), or if deliverability issues arise with Resend.
- Review trigger: If email bounce rate exceeds 5%, or if users report missing transactional emails post-migration.
- Monitoring: Email delivery rate, bounce rate, open rate, click rate, send latency (p99), weekly digest delivery success rate, Magic Link email delivery time.
Consequences
Positive: - Eliminates P0 tech debt
(lutung deprecated library) - Single email provider across
Peeq + Rival platforms (Resend) - Unified delivery tracking for all
email types (including Magic Link auth) - Simpler operations (1 Resend
account vs 4 Mandrill accounts) - Code-based templates (version
controlled, previewable) - Modern API with webhook-based delivery
tracking - React Email support for template development (aligns with
ADR-008 React migration) - CortexOne function available for batch/async
email workloads
Negative: - Migration effort (templates, webhook handlers, delivery status tracking) - Resend is newer/smaller company than Mailchimp — vendor risk - Need to set up domain authentication (SPF, DKIM, DMARC) for Resend - Existing Mandrill webhook callback format differs from Resend — handler rewrite needed - CortexOne email function adds a dependency on Rival infrastructure for batch emails
Mitigated by: Resend is built on Amazon SES (proven infrastructure). Domain auth is one-time setup. Webhook handler is straightforward REST endpoint. Phase 1 (library replacement) can be done independently of later phases. CortexOne dependency is optional — batch emails can run from Java service.
Decision date: 2026-01-31 Review by: 2026-07-31