Architecture Review: Notification Delivery Platform
A multi-channel notification delivery architecture that accepts upstream business events (order placed, payment received, comment posted, threshold alert triggered) and routes them to per-channel delivery workers (push via FCM/APNs, email via SendGrid, SMS via Twilio, in-app via WebSocket). Kafka carries raw business events from upstream producers. RabbitMQ handles per-channel fan-out with separate exchanges and queues per delivery channel, isolating email queue backlog from push notification delivery. PostgreSQL provides durable notification state tracking (sent, failed, bounced, suppressed). Redis enforces per-user rate limiting (notification frequency caps to prevent fatigue) and stores deduplication tokens to prevent duplicate sends across retry attempts. The inbox pattern on the consumer side ensures idempotent delivery even when Kafka produces duplicate events.
Evidence Confidence
Moderate
strong
Executive Summary
Notification Delivery Platform: limited operational readiness (81% evidence confidence). 0 architectural strengths identified, 5 operational risks to manage. Primary concern: Queue Backlog Accumulation. Requires Advanced operational maturity.
Readiness Rationale
Overall limited readiness across 8 dimensions. Weak: consistency. Limited: scaling, team maturity. Strong: migration, observability, topology resilience.
Key Concerns
- !Queue Backlog Accumulation
- !Rate Limit Cascade
Key Strengths
- +Architecture is well-defined for the event driven system problem profile
8
Assessments
2
Tradeoffs
6
Sections
11
Recommendations
Readiness Assessments
8Architectural Tradeoffs
2Recommendations
11Monitor: Queue Backlog Accumulation
risk_monitoringMessage queue or event stream consumer processing rate falls below producer write rate, causing consumer lag to grow unboundedly: eventually leading to increased end-to-end latency, producer backpressure, data expiry, or queue resource exhaustion.
Affects 2 nodes. (Event Streaming, Slow Consumer)
Monitor: Rate Limit Cascade
risk_monitoringWhen a downstream service begins rate limiting requests from an upstream service, the upstream's retry logic with insufficient backoff amplifies the request rate : exceeding the rate limit further and potentially pushing the rejection downstream to other upstream callers, producing a cascade of rate-limited retries across the call graph.
Affects 0 nodes
Implement: Monitor queue backlog signals
observabilitySeed 'Queue Consumer Backlog' identifies 4 metrics relevant to queue_backlog_accumulation.
Metrics to instrument: queue_depth, consumer_lag_seconds, consumer_throughput
Direct synchronous notification sends in application code (inline with business transaction) → Kafka-decoupled async notification pipeline with RabbitMQ per-channel fan-out
migration_planningTrigger: Notification provider latency (SendGrid, FCM) adding 200–500ms to business transaction p99 latency; a provider outage causing business transaction failures (e.g., order placement failing because email send fails); no ability to retry failed notifications without replaying the entire business transaction. Migrate from 'Direct synchronous notification sends in application code (inline with business transaction)' to 'Kafka-decoupled async notification pipeline with RabbitMQ per-channel fan-out'. Deploy the Kafka event publication before removing the synchronous send code. Run both in parallel for 1 week: publish to Kafka AND send synchronously, validating that the Kafka path delivers the same notifications. Remove the synchronous path only after the Kafka path is validated and the inbox deduplication layer is live.
The application code change from synchronous send to event publication requires careful handling of cases where the business logic previously used the notification send result (e.g., "if email send fails, block the operation"): these cases must be explicitly identified and decoupled; Until the inbox pattern is in place, the async pipeline has at-least-once delivery with no deduplication; early deployments may produce duplicate notifications if upstream events are retried
Single-channel notification delivery (email only) → Multi-channel notification delivery (email + push + SMS + in-app) with per-channel RabbitMQ queues
migration_planningTrigger: Product requirement to add mobile push notifications and SMS for critical transactional alerts; need to route different notification types to different channels based on user preference; single delivery code path unable to handle channel-specific retry semantics and rate limits. Migrate from 'Single-channel notification delivery (email only)' to 'Multi-channel notification delivery (email + push + SMS + in-app) with per-channel RabbitMQ queues'. Add channels one at a time. Start with in-app notifications (no external provider dependency) to validate the RabbitMQ channel routing architecture, then add push, then SMS. Each channel requires its own dead-letter queue configuration and delivery monitoring dashboard before it is considered production-ready.
Each new channel introduces a new provider dependency with its own authentication, rate limiting, and failure mode; adding push before implementing per-channel circuit breakers risks a FCM outage cascading to the entire notification pipeline; Mobile push token management (FCM/APNs token refresh, invalid token handling) is operationally complex: invalid tokens must be removed from delivery attempts and their invalid status must be recorded to prevent repeated failed delivery attempts
Prepare runbook for: Burst Traffic Cold Cache Stampede
simulation_preparednessSimulation demonstrates critical degradation of redis, postgresql
Without a runbook, recovery from this failure mode will be ad-hoc
Prepare runbook for: Connection Pool Exhaustion with Horizontal User Scale
simulation_preparednessSimulation demonstrates critical degradation of postgresql
Without a runbook, recovery from this failure mode will be ad-hoc
Plan evolution: OLTP Analytics Queries → OLTP + OLAP Separation
evolution_planningEvolution from Unified OLTP + Analytics on PostgreSQL → Separated OLTP (PostgreSQL) + OLAP (ClickHouse/Snowflake)
Migration complexity: medium. Rollback: always.
Plan evolution: Single Cache Layer → Distributed Cache
evolution_planningEvolution from Single Redis Node / Sentinel Cluster → Distributed Redis Cluster (Consistent Hash Ring)
Migration complexity: medium. Rollback: complex.
Monitor threshold: Tier 1: Provider Rate Limit Backlog
scaling_monitoringSignal: RabbitMQ queue depth for email channel growing > 100k messages; SendGrid 429 responses visible in delivery worker logs; email delivery p95 latency > 2 minutes; delivery worker retry thread pool saturated; dead-letter queue receiving messages from retry exhaustion despite provider being available
Bottleneck: Delivery worker retry policy not aligned with provider rate limit reset window; workers retrying before the rate limit has reset, accumulating failed attempts. Evolution: Implement provider-aware retry backoff: on 429 response, parse the Retry-After header and schedule the next retry attempt at exactly that time, not on a fixed exponential backoff schedule; add per-provider circuit breakers that open after 5 consecutive 429s and attempt a probe request at the retry-after interval; scale email consumer replicas to process the backlog faster when the rate limit window resets
Monitor threshold: Tier 2: Notification Fan-Out Amplification
scaling_monitoringSignal: Kafka consumer lag for notification event consumers growing; notification delivery volume much higher than upstream business event volume (ratio > 5:1); RabbitMQ aggregate message rate across all channel queues elevated; one upstream event type (e.g., new_comment) accounting for disproportionate share of notification volume
Bottleneck: Upstream event fan-out generating more notification sends per event than expected; or a notification rule misconfiguration triggering notifications for every event regardless of user preference. Evolution: Audit notification fan-out ratio per upstream event type; add fan-out cost metrics (notifications_generated per upstream event) as a monitored SLA; implement notification preference filtering before fan-out: only generate delivery attempts for users with the corresponding notification type enabled; implement a notification aggregation layer that batches multiple low-priority events into digest notifications rather than individual sends
Scaling Pressure Signals
8RabbitMQ queue depth for email channel growing > 100k messages; SendGrid 429 responses visible in delivery worker logs; email delivery p95 latency > 2 minutes; delivery worker retry thread pool saturated; dead-letter queue receiving messages from retry exhaustion despite provider being available
Threshold
Tier 1: Provider Rate Limit Backlog
Likely Bottleneck
Delivery worker retry policy not aligned with provider rate limit reset window; workers retrying before the rate limit has reset, accumulating failed attempts
Recommended Evolution
Implement provider-aware retry backoff: on 429 response, parse the Retry-After header and schedule the next retry attempt at exactly that time, not on a fixed exponential backoff schedule; add per-provider circuit breakers that open after 5 consecutive 429s and attempt a probe request at the retry-after interval; scale email consumer replicas to process the backlog faster when the rate limit window resets
Kafka consumer lag for notification event consumers growing; notification delivery volume much higher than upstream business event volume (ratio > 5:1); RabbitMQ aggregate message rate across all channel queues elevated; one upstream event type (e.g., new_comment) accounting for disproportionate share of notification volume
Threshold
Tier 2: Notification Fan-Out Amplification
Likely Bottleneck
Upstream event fan-out generating more notification sends per event than expected; or a notification rule misconfiguration triggering notifications for every event regardless of user preference
Recommended Evolution
Audit notification fan-out ratio per upstream event type; add fan-out cost metrics (notifications_generated per upstream event) as a monitored SLA; implement notification preference filtering before fan-out: only generate delivery attempts for users with the corresponding notification type enabled; implement a notification aggregation layer that batches multiple low-priority events into digest notifications rather than individual sends
PostgreSQL inbox table row count > 500M; inbox deduplication query latency > 10ms (above the acceptable delivery path overhead); inbox table VACUUM running continuously; index bloat on notification_id index visible in pg_stat_user_indexes; dead tuple count in pg_stat_user_tables for inbox table growing faster than autovacuum can clear
Threshold
Tier 3: Inbox Table Growth and Query Pressure
Likely Bottleneck
Inbox table accumulating rows beyond the effective autovacuum throughput; index bloat from high update rate on delivery_state column
Recommended Evolution
Partition the inbox table by created_at date range; implement automated partition drop for partitions older than the deduplication TTL (e.g., drop partitions > 7 days old); this replaces row-level DELETE with partition DROP, which is orders of magnitude faster; separate the delivery_state tracking table from the deduplication inbox table to reduce update churn on the primary dedup index
Single provider (SendGrid or Twilio) outage causing sustained delivery failure across all email or SMS notifications; no automatic fallback to secondary provider; delivery SLA breach for transactional notifications (password reset, payment confirmation) during provider maintenance window; provider cost becoming material and single-vendor lock-in a business risk
Threshold
Tier 4: Multi-Provider Delivery Architecture
Likely Bottleneck
Single provider dependency with no failover path; delivery workers not routing around degraded providers
Recommended Evolution
Implement provider abstraction layer with per-channel provider routing config; add a secondary provider (e.g., Postmark as email fallback behind SendGrid) with circuit-breaker-driven automatic failover; route transactional notifications (priority=critical) through the primary provider with automatic failover to secondary on circuit open; route marketing notifications through the cheaper secondary provider with no failover (acceptable to drop marketing notifications during outages)
RabbitMQ queue depth for email channel growing > 100k messages; SendGrid 429 responses visible in delivery worker logs; email delivery p95 latency > 2 minutes; delivery worker retry thread pool saturated; dead-letter queue receiving messages from retry exhaustion despite provider being available
Threshold
Escalation trigger: Delivery worker retry policy not aligned with provider rate limit reset window; workers retrying before the rate limit has reset, accumulating failed attempts
Likely Bottleneck
Tier 1: Provider Rate Limit Backlog
Recommended Evolution
Monitor: queue_depth, consumer_lag_seconds, consumer_throughput
Kafka consumer lag for notification event consumers growing; notification delivery volume much higher than upstream business event volume (ratio > 5:1); RabbitMQ aggregate message rate across all channel queues elevated; one upstream event type (e.g., new_comment) accounting for disproportionate share of notification volume
Threshold
Escalation trigger: Upstream event fan-out generating more notification sends per event than expected; or a notification rule misconfiguration triggering notifications for every event regardless of user preference
Likely Bottleneck
Tier 2: Notification Fan-Out Amplification
Recommended Evolution
Monitor: queue_depth, consumer_lag_seconds, consumer_throughput
PostgreSQL inbox table row count > 500M; inbox deduplication query latency > 10ms (above the acceptable delivery path overhead); inbox table VACUUM running continuously; index bloat on notification_id index visible in pg_stat_user_indexes; dead tuple count in pg_stat_user_tables for inbox table growing faster than autovacuum can clear
Threshold
Escalation trigger: Inbox table accumulating rows beyond the effective autovacuum throughput; index bloat from high update rate on delivery_state column
Likely Bottleneck
Tier 3: Inbox Table Growth and Query Pressure
Recommended Evolution
Monitor: queue_depth, consumer_lag_seconds, consumer_throughput
Single provider (SendGrid or Twilio) outage causing sustained delivery failure across all email or SMS notifications; no automatic fallback to secondary provider; delivery SLA breach for transactional notifications (password reset, payment confirmation) during provider maintenance window; provider cost becoming material and single-vendor lock-in a business risk
Threshold
Escalation trigger: Single provider dependency with no failover path; delivery workers not routing around degraded providers
Likely Bottleneck
Tier 4: Multi-Provider Delivery Architecture
Recommended Evolution
Monitor: queue_depth, consumer_lag_seconds, consumer_throughput
Migration Readiness
12Migration Stages
3Direct synchronous notification sends in application code (inline with business transaction) → Kafka-decoupled async notification pipeline with RabbitMQ per-channel fan-out
infoMigration trigger: Notification provider latency (SendGrid, FCM) adding 200–500ms to business transaction p99 latency; a provider outage causing business transaction failures (e.g., order placement failing because email send fails); no ability to retry failed notifications without replaying the entire business transaction
Single-channel notification delivery (email only) → Multi-channel notification delivery (email + push + SMS + in-app) with per-channel RabbitMQ queues
infoMigration trigger: Product requirement to add mobile push notifications and SMS for critical transactional alerts; need to route different notification types to different channels based on user preference; single delivery code path unable to handle channel-specific retry semantics and rate limits
Fixed notification preference (all users receive all notification types) → Per-user notification preference management with suppression and rate limiting
infoMigration trigger: User complaints about notification volume increasing; spam classification rate rising for marketing notifications; regulatory requirement (GDPR, CAN-SPAM) to honor notification opt-out within 10 business days; need to suppress notifications for churned users to avoid wasting provider quota
Risks
9The application code change from synchronous send to event p
warningThe application code change from synchronous send to event publication requires careful handling of cases where the business logic previously used the notification send result (e.g., "if email send fails, block the operation"): these cases must be explicitly identified and decoupled
Until the inbox pattern is in place, the async pipeline has
warningUntil the inbox pattern is in place, the async pipeline has at-least-once delivery with no deduplication; early deployments may produce duplicate notifications if upstream events are retried
Each new channel introduces a new provider dependency with i
warningEach new channel introduces a new provider dependency with its own authentication, rate limiting, and failure mode; adding push before implementing per-channel circuit breakers risks a FCM outage cascading to the entire notification pipeline
Mobile push token management (FCM/APNs token refresh, invali
warningMobile push token management (FCM/APNs token refresh, invalid token handling) is operationally complex: invalid tokens must be removed from delivery attempts and their invalid status must be recorded to prevent repeated failed delivery attempts
Preference enforcement at the fan-out stage (before queue in
warningPreference enforcement at the fan-out stage (before queue insertion) is correct but requires reading user preference for every event; at high event volume, this becomes a high-read-rate workload on PostgreSQL user preferences table: Redis caching of preferences is mandatory
Transactional notifications must be explicitly excluded from
warningTransactional notifications must be explicitly excluded from user preference suppression; the preference system must support a non-suppressible tier that bypasses all user-level rate limiting
Projection lag creates a read-after-write window where users
criticalProjection lag creates a read-after-write window where users see stale data after their own writes. Mitigation: Route immediate post-write reads to the write store (session-scoped write token); accept eventual consistency only for non-user-initiated reads
↗ direct-db-to-cqrs
Projection rebuild after schema change can take hours or day
criticalProjection rebuild after schema change can take hours or days on large datasets. Mitigation: Design blue/green projection deployment: build new projection in parallel before switching traffic; test rebuild time in staging
↗ direct-db-to-cqrs
Cross-service workflows that previously used database transa
criticalCross-service workflows that previously used database transactions now require Saga orchestration. Mitigation: Design idempotent event handlers; implement compensating transactions for every multi-step workflow; test failure injection in staging
↗ modular-monolith-to-event-driven
Review Sections
6Referenced Intelligence