ADR

ADR-012: Email Delivery Strategy — Mandrill Dependency Reduction

Last updated: 2026-02-01 | Decisions

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
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).

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.

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

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

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