1. Problem and Motivation
In modern digital products, communication with users is essential: authentication depends on One-Time Passwords (OTPs), marketplaces depend on order confirmations, financial apps depend on fraud alerts, and social platforms depend on real-time activity notifications.
While sending an individual email or push notification via an API SDK (such as SendGrid or Twilio) requires only ten lines of code, orchestrating a centralized notification platform at enterprise scale—dispatching 100 million messages daily across multiple global channels—introduces complex distributed systems challenges:
+---------------------------------------------------------------------------------------------------+
| ENTERPRISE NOTIFICATION COMPLEXITIES |
+---------------------------------------------------------------------------------------------------+
| 1. Channel Diversity: APNs (Apple), FCM (Google), SMS (Twilio, Infobip), Email (SES, SendGrid), |
| In-App WebSockets, and Slack/Teams webhooks each have wildly different protocols and SLAs. |
| 2. Unpredictable Traffic Surges: A flash sale or breaking news alert enqueues 15,000,000 pushes |
| instantaneously, threatening to overwhelm internal brokers and third-party telecom gateways. |
| 3. Priority Inversion (Head-of-Line Blocking): A marketing broadcast must never block a |
| sub-second password reset OTP or emergency security alert. |
| 4. Strict Idempotency: Network retries or message queue re-deliveries must never charge a user's |
| credit card twice or send five identical SMS text messages. |
| 5. Downstream Vendor Throttling: External vendors enforce hard rate limits (e.g., Twilio 100/sec, |
| SES 500/sec). Exceeding these limits yields HTTP 429 errors, message loss, and penalty fees. |
| 6. User Preferences & Compliance: GDPR opt-outs, quiet hours (10:00 PM - 08:00 AM in user local |
| timezone), and frequency caps (e.g., max 3 marketing emails/week) must be enforced instantly. |
+---------------------------------------------------------------------------------------------------+
2. Mental Model & Architectural Overview
The notification platform decouples Ingestion & Validation, Priority-Based Buffering, Worker Dispatch & Throttling, and Delivery Tracking into independent asynchronous stages.
1. Upstream Services & Deduplication Gate
Establishes the ingress and structural boundary across Upstream Services & Deduplication Gate: Auth, Orders, Marketing POST /api/v1/notify; Redis SETNX (dedup_key, TTL) Suppresses duplicate retries; Window: 10m - 24h.
2. P1 Queue: Transactional & P2 Queue: Bulk Marketing
Coordinates core data-plane execution and state transitions through P1 Queue: Transactional & P2 Queue: Bulk Marketing: SLA: < 30 seconds; Order confirmation, payments; Auto-scaling worker group.
3. Dead Letter Queue (DLQ) & Apple APNs & Google FCM
Enforces durability, state convergence, and output verification across Dead Letter Queue (DLQ) & Apple APNs & Google FCM: Max 5 retries exceeded Invalid device tokens / dead emails Auto-unregisters dead APNs tokens; HTTP/2 Persistent Multiplexing Push Notifications; SMPP / HTTP REST API Dynamic Carrier Failover.
The Core Stages:
- Ingestion API & Filter Layer:
- Upstream services (Auth, Checkout, Social Feed) submit notification requests via
POST /api/v1/notify. - Distributed Deduplication Gate: Computes an idempotency hash and checks Redis using an atomic
SETNXlease. If a duplicate is detected within the deduplication window (e.g., 10 minutes), the request is dropped immediately. - User Preference & Quiet Hours Engine: Queries the user preference cache. If the user opted out of the channel, or if the current timestamp falls within the user’s localized quiet hours (and the message is not high-priority), the message is either suppressed or rescheduled into a delayed queue.
- Priority Classification: Tags the message with a strict priority tier (, , ).
- Upstream services (Auth, Checkout, Social Feed) submit notification requests via
- Partitioned Priority Queues:
- Instead of a single shared FIFO queue, the platform partitions traffic into separate queues:
- (Urgent / Security): Auth OTPs, fraud alerts, password resets. Target SLA: .
- (Transactional): Order confirmations, flight changes, direct chat messages. Target SLA: .
- (Promotional / Bulk): Marketing campaigns, weekly digests, re-engagement emails. Target SLA: .
- Instead of a single shared FIFO queue, the platform partitions traffic into separate queues:
- Dispatch Workers & Distributed Rate Limiting:
- Independent auto-scaling worker pools consume from each queue tier.
- Before invoking downstream third-party APIs, workers query a Distributed Token Bucket Rate Limiter (backed by Redis Lua scripts) to guarantee cluster-wide compliance with vendor transactions-per-second (TPS) contracts.
- Carrier Adapters, Circuit Breakers & Dynamic Failover:
- Normalized vendor adapters translate payloads into vendor-specific HTTP/2 or SMPP formats.
- A Circuit Breaker monitors vendor error rates ( spikes or carrier latencies). If Primary SMS Vendor (e.g., Twilio) degrades, traffic fails over automatically to Secondary SMS Vendor (e.g., MessageBird or Infobip).
- Reconciliation & Status Webhooks:
- Downstream providers send asynchronous HTTP webhooks reporting delivery status (
DELIVERED,BOUNCED,REJECTED). - A webhook ingestion gateway reconciles the delivery lifecycle in distributed storage (Cassandra / DynamoDB) and automatically deactivates invalid push tokens.
- Downstream providers send asynchronous HTTP webhooks reporting delivery status (
Architectural Blueprint: Distributed Task Scheduler (Hierarchical Timing Wheels vs. SKIP LOCKED)
Delayed and recurring notifications (e.g., “send reminder in 25 minutes”, “execute billing cron every hour”) require a fault-tolerant Distributed Task Scheduler:
- Database-Backed Work Claiming (
SELECT ... FOR UPDATE SKIP LOCKED):- For schedules up to ~, sharded PostgreSQL tables indexed on
(status, scheduled_at)allow stateless worker nodes to claim due tasks without lock contention:UPDATE scheduled_jobs SET status = 'RUNNING', locked_by = $1, locked_until = NOW() + INTERVAL '60 seconds' WHERE job_id IN ( SELECT job_id FROM scheduled_jobs WHERE status = 'PENDING' AND scheduled_at <= NOW() ORDER BY scheduled_at ASC LIMIT 100 FOR UPDATE SKIP LOCKED ) RETURNING *; SKIP LOCKEDimmediately skips rows currently locked by peer workers, turning a relational table into a high-concurrency lock-free work queue.
- For schedules up to ~, sharded PostgreSQL tables indexed on
- In-Memory Hierarchical Timing Wheels ( Insertion & Cancellation):
- Tracking millions of active timers in a min-heap costs per insert/cancel. Hashed & Hierarchical Timing Wheels (Varghese & Lauck, used in Kafka, Netty, and Linux kernel timers) organize time into circular ring slots (e.g., Wheel 1: slots; Wheel 2: slots; Wheel 3: slots) backed by an append-only WAL.
- Advancing the current tick pointer costs amortized time, cascading overflowing high-horizon buckets down into finer wheels as deadlines approach.
3. Terminology & Core Concepts
- APNs (Apple Push Notification service): Apple’s binary HTTP/2 multiplexed protocol for sending push notifications to iOS and macOS devices.
- FCM (Firebase Cloud Messaging): Google’s cross-platform messaging service for Android, iOS, and Web.
- SMPP (Short Message Peer-to-Peer): An industry-standard telecommunications protocol used by SMS aggregators to exchange SMS messages directly with mobile network operators.
- Idempotency Key: A unique deterministic string derived from the event metadata used to guarantee that identical requests received multiple times produce exactly one external action.
- Head-of-Line (HoL) Blocking: A performance hazard where a queue is monopolized by a massive slow task (e.g., a 10M newsletter blast), preventing subsequent urgent tasks (e.g., login OTPs) from being processed.
- Token Bucket Algorithm: A rate-limiting algorithm that maintains a bucket of tokens refilled at a constant rate ; each request consumes 1 token. It allows bursts up to the bucket capacity while enforcing an average rate limit .
- Dead Letter Queue (DLQ): A dedicated queue holding messages that have repeatedly failed processing after exhaustive retries, isolating poison-pill messages for engineering inspection.
4. Architectural Deep Dive: Deduplication & Priority Isolation
1. Distributed Deduplication (Preventing Double-Dispatch)
Distributed message brokers (Kafka, RabbitMQ, Amazon SQS) operate under At-Least-Once delivery semantics. If a network blip occurs after a worker finishes sending an SMS but before it commits the queue offset, the message broker re-delivers the message to a different worker.
Without deduplication, the customer receives two identical SMS messages and the company pays double telecom fees.
Incoming Request
|
v
+-----------------------------------------------------------+
| Deduplication Key: SHA256(user_id + channel + event_hash) |
+-----------------------------------------------------------+
|
v
+-----------------------------------------------------------+
| Redis: SET idempotency:key "LOCKED" NX EX 600 |
+-----------------------------------------------------------+
| |
| Key Acquired (1) | Key Exists (0)
v v
+-----------------------+ +------------------------------------+
| Dispatch Notification | | Duplicate Detected: Drop / ACK OK |
+-----------------------+ +------------------------------------+
The deduplication lease uses an atomic Redis command:
SET notify:dedup:<user_id>:<channel>:<template_id>:<hash> "IN_FLIGHT" NX EX 600
- If Redis returns
OK: The worker successfully acquired the unique execution lease. - If Redis returns
nil: Another worker is currently processing or has already completed this dispatch within the 600-second window. The worker drops the duplicate and acknowledges the broker.
2. Design Alternatives & Trade-Off Matrix: Queue Isolation, Deduplication, and Failover
Consider what happens when a single queue is used during a Black Friday promotional campaign:
If User Bob attempts to log in at 09:05 AM ( into the blast, after messages have drained) and requests a Two-Factor Authentication OTP, his OTP message is placed behind the remaining items in the queue. Bob’s OTP will wait —guaranteeing authentication failure.
+---------------------------------------------------------------------------------------------------+
| PRIORITY TIER ISOLATION TOPOLOGY |
+---------------------------------------------------------------------------------------------------+
| [P0: Urgent Queue] -----> Dedicated Pool 0 (50 pods) -----> Target SLA: < 2 sec (OTP / Auth) |
| [P1: Normal Queue] -----> Dedicated Pool 1 (100 pods) -----> Target SLA: < 30 sec (Orders) |
| [P2: Bulk Queue] -----> Auto-scaled Pool 2 (20 pods) -----> Target SLA: < 4 hrs (Marketing) |
+---------------------------------------------------------------------------------------------------+
By decoupling into distinct physical queues with dedicated worker pools, queues remain permanently near-zero depth regardless of marketing volume.
| Architectural Dimension | Naive / Alternative Approach | Production Notification Pattern | Primary Trade-Off |
|---|---|---|---|
| Priority Scheduling | Single shared FIFO queue or in-broker priority heap | Physically isolated topics (notify-p0, notify-p1, notify-p2) with dedicated worker pools | Slightly lower idle worker utilization in exchange for zero Head-of-Line blocking on OTP SLAs |
| Duplicate Prevention | Relying solely on Kafka consumer offset commits | Atomic Redis SET key NX EX ttl idempotency lease + persistent status check | Extra sub-millisecond Redis RTT per dispatch in exchange for preventing double-billing during worker crashes |
| Vendor Rate Shaping | Per-pod local in-memory rate limiters | Centralized Redis Lua Token Bucket + jittered exponential backoff | Requires highly available Redis shard, but enforces exact cluster-wide TPS even during pod auto-scaling |
| Carrier Resilience | Hardcoded single SMS/email aggregator | Multi-vendor adapter layer guarded by per-carrier 3-state circuit breakers | Requires maintaining normalized templates and contracts across carriers |
5. Mathematical Models: Token Bucket Dynamics, Retry Jitter & Queue Sizing
1. Token Bucket Rate Limiter Formulation
Telecommunication gateways (e.g., Twilio, Amazon SES) strictly penalize rate spikes. Let:
- : Token generation rate (tokens per second)
- : Maximum token bucket burst capacity
- : Number of available tokens at time
- : Elapsed time since last token replenishment
The available token capacity is updated via:
If : The token request succeeds, , and dispatch proceeds. Otherwise, the request is delayed or rejected with backoff.
Worked Example: SMS Gateway Throttling
A telecommunications aggregator permits an average rate of with an allowable burst . Assuming the bucket starts full (), if an upstream microservice abruptly dumps SMS messages simultaneously into the worker pool:
- The first messages consume the pre-accumulated burst tokens and are dispatched immediately ().
- The remaining messages must wait for tokens to replenish at the steady rate .
- Total time to clear the burst:
2. Queue Backlog & Sizing Derivation
Let be the arrival rate of notifications and be the service rate per worker node. For a cluster of active worker pods:
The instantaneous queue growth rate is:
If a flash marketing campaign generates an arrival spike of for , and our downstream vendor rate limiter caps processing at :
If each serialized message payload consumes (, decimal) in Apache Kafka:
Once the 60-second campaign burst finishes and background traffic returns to , the backlog drains at net rate , clearing in (). This proves why Apache Kafka’s sequential disk-backed log architecture is essential: pure in-memory queues risk out-of-memory (OOM) eviction under multi-million message backlogs.
3. Exponential Backoff with Full Jitter
When downstream carriers return transient HTTP 429 or HTTP 503 responses, deterministic exponential backoff () causes all throttled workers to sleep for the exact same duration and collide again in synchronized retry waves. Production workers draw a uniform random wait using Full Jitter:
where is the retry attempt index, is initial backoff (e.g., ), and is maximum delay (e.g., ). At attempt , the window ceiling is with expected wait . Across throttled notifications, Full Jitter spreads retries uniformly over () rather than firing an instantaneous -request spike at .
6. Dynamic Carrier Failover & Circuit Breakers
SMS delivery across global telecommunications is notoriously brittle: regional telecom carriers frequently experience fiber cuts, routing errors, or regulatory filtering.
An enterprise notification engine implements an automated Three-State Circuit Breaker per downstream vendor:
+-------------------------+
| |
| CLOSED | (Normal Operation: 100% Traffic to Vendor A)
| |
+-------------------------+
|
| Error rate > 15% or Latency > 2500ms
v
+-------------------------+
| |
| OPEN | (Tripped: 100% Traffic routed to Vendor B)
| |
+-------------------------+
|
| Cooldown period elapsed (60 seconds)
v
+-------------------------+
| |
| HALF-OPEN | (Probe: Send 5% trial traffic to Vendor A)
| |
+-------------------------+
/ \
Success rate >= 98% Probe fails
/ \
v v
CLOSED OPEN
7. Production Implementation: Resilient Notification Engine
The following complete TypeScript implementation demonstrates:
- Distributed deduplication with Redis
SETNX. - Priority queue router.
- Redis Lua-based Token Bucket rate limiting.
- Circuit Breaker carrier failover.
/**
* Production Distributed Notification Engine
* Deduplication, Token Bucket Limiting, and Carrier Failover.
*/
export type Priority = 'P0_URGENT' | 'P1_TRANSACTIONAL' | 'P2_BULK';
export type Channel = 'PUSH' | 'SMS' | 'EMAIL';
export interface NotificationPayload {
notificationId: string;
userId: string;
channel: Channel;
priority: Priority;
recipient: string; // phone number, email, or device token
templateId: string;
templateArgs: Record<string, string>;
idempotencyWindowSeconds?: number;
}
export interface DeliveryResult {
status: 'DELIVERED' | 'DUPLICATE_SUPPRESSED' | 'THROTTLED' | 'FAILED';
provider: string;
latencyMs: number;
messageId?: string;
error?: string;
}
/**
* Distributed Token Bucket Rate Limiter
*/
export class TokenBucketLimiter {
private tokens: number;
private lastRefillTimestamp: number;
constructor(
private readonly capacity: number,
private readonly refillRatePerSecond: number
) {
this.tokens = capacity;
this.lastRefillTimestamp = Date.now();
}
public tryAcquire(requestedTokens = 1): boolean {
const now = Date.now();
const elapsedSeconds = (now - this.lastRefillTimestamp) / 1000;
this.lastRefillTimestamp = now;
// Replenish tokens up to max capacity
this.tokens = Math.min(
this.capacity,
this.tokens + elapsedSeconds * this.refillRatePerSecond
);
if (this.tokens >= requestedTokens) {
this.tokens -= requestedTokens;
return true;
}
return false; // Throttled
}
}
/**
* Three-State Circuit Breaker for Carrier Gateways
*/
export class CarrierCircuitBreaker {
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private failureCount = 0;
private successCount = 0;
private lastStateChange = Date.now();
constructor(
public readonly name: string,
private readonly failureThreshold = 5,
private readonly recoveryTimeMs = 30_000
) {}
public canExecute(): boolean {
const now = Date.now();
if (this.state === 'OPEN') {
if (now - this.lastStateChange >= this.recoveryTimeMs) {
this.state = 'HALF_OPEN';
this.lastStateChange = now;
return true; // Allow trial probe
}
return false; // Fast fail
}
return true;
}
public recordSuccess(): void {
if (this.state === 'HALF_OPEN') {
this.successCount += 1;
if (this.successCount >= 3) {
this.state = 'CLOSED';
this.failureCount = 0;
this.successCount = 0;
}
} else {
this.failureCount = 0;
}
}
public recordFailure(): void {
this.failureCount += 1;
this.lastStateChange = Date.now();
if (this.failureCount >= this.failureThreshold || this.state === 'HALF_OPEN') {
this.state = 'OPEN';
}
}
public getState(): string {
return this.state;
}
}
/**
* Enterprise Notification Dispatch Service
*/
export class NotificationDispatcher {
constructor(
private readonly redisMock: Map<string, string>,
private readonly rateLimiter: TokenBucketLimiter,
private readonly primaryCarrierBreaker: CarrierCircuitBreaker,
private readonly fallbackCarrierBreaker: CarrierCircuitBreaker
) {}
public async dispatch(payload: NotificationPayload): Promise<DeliveryResult> {
const start = Date.now();
// 1. Distributed Deduplication Check (SETNX simulation)
const dedupKey = `dedup:${payload.userId}:${payload.channel}:${payload.templateId}`;
if (this.redisMock.has(dedupKey)) {
return {
status: 'DUPLICATE_SUPPRESSED',
provider: 'NONE',
latencyMs: Date.now() - start,
};
}
// 2. Token Bucket Rate Limiter Check (before persisting dedupKey so throttled retries are not dropped)
if (!this.rateLimiter.tryAcquire(1)) {
return {
status: 'THROTTLED',
provider: 'NONE',
latencyMs: Date.now() - start,
error: 'Downstream vendor TPS ceiling reached. Re-queueing with backoff.',
};
}
// 3. Carrier Selection & Circuit Breaker Execution
let selectedCarrier = 'PrimaryCarrier';
let breaker = this.primaryCarrierBreaker;
if (!breaker.canExecute()) {
// Primary carrier is OPEN (degraded) -> Failover to secondary
selectedCarrier = 'SecondaryCarrier';
breaker = this.fallbackCarrierBreaker;
if (!breaker.canExecute()) {
return {
status: 'FAILED',
provider: 'ALL_CIRCUITS_OPEN',
latencyMs: Date.now() - start,
error: 'Both primary and secondary telecom gateways are down.',
};
}
}
// Lock deduplication key in Redis once rate limiter and circuit breaker admit the request
this.redisMock.set(dedupKey, payload.notificationId);
// 4. Simulate External Carrier Network Call
try {
const messageId = await this.sendToVendor(selectedCarrier, payload);
breaker.recordSuccess();
return {
status: 'DELIVERED',
provider: selectedCarrier,
messageId,
latencyMs: Date.now() - start,
};
} catch (err) {
this.redisMock.delete(dedupKey);
breaker.recordFailure();
return {
status: 'FAILED',
provider: selectedCarrier,
error: (err as Error).message,
latencyMs: Date.now() - start,
};
}
}
private async sendToVendor(
carrier: string,
payload: NotificationPayload
): Promise<string> {
// In production: HTTP/2 call to APNs, or REST call to Twilio/SendGrid
return `msg_${carrier}_${Date.now()}_${payload.notificationId.slice(0, 8)}`;
}
}
async function runDemo(): Promise<void> {
console.log("=== Notification Platform: Priority Queue & Circuit Breakers ===");
const redisMock = new Map<string, string>();
const rateLimiter = new TokenBucketLimiter(10, 5); // 10 capacity, 5 refill/sec
const primaryBreaker = new CarrierCircuitBreaker("sendgrid", 2, 5000);
const fallbackBreaker = new CarrierCircuitBreaker("amazon-ses", 2, 5000);
const dispatcher = new NotificationDispatcher(redisMock, rateLimiter, primaryBreaker, fallbackBreaker);
console.log("Dispatcher initialized with Token Bucket limiter and SendGrid/SES circuit breakers");
const payload: NotificationPayload = {
notificationId: "notif-101",
userId: "user-42",
channel: "EMAIL",
priority: "P1_TRANSACTIONAL",
recipient: "alice@example.com",
templateId: "welcome-email",
templateArgs: { firstName: "Alice" },
idempotencyWindowSeconds: 600,
};
const first = await dispatcher.dispatch(payload);
console.log(`First dispatch: status=${first.status}, provider=${first.provider}, breaker=${primaryBreaker.getState()}`);
const duplicate = await dispatcher.dispatch(payload);
console.log(`Retry dispatch: status=${duplicate.status}, provider=${duplicate.provider}`);
primaryBreaker.recordFailure();
primaryBreaker.recordFailure();
const failoverPayload: NotificationPayload = {
...payload,
notificationId: "notif-102",
templateId: "otp-login",
priority: "P0_URGENT",
};
const failoverResult = await dispatcher.dispatch(failoverPayload);
console.log(`Failover dispatch: status=${failoverResult.status}, provider=${failoverResult.provider}, primaryBreaker=${primaryBreaker.getState()}`);
}
runDemo();
8. Asynchronous Delivery Reconciliation & Webhooks
Transmitting a notification to a downstream provider only confirms that the provider accepted the payload (HTTP 202 Accepted). It does not confirm that the SMS reached the cell tower or that the push notification rendered on the iPhone screen.
+---------------------------------------------------------------------------------------------------+
| DELIVERY LIFECYCLE STATE MACHINE |
+---------------------------------------------------------------------------------------------------+
| [SUBMITTED] -----> [ENQUEUED] -----> [SENT_TO_GATEWAY] -----> [DELIVERED_TO_DEVICE] |
| | |
| v |
| [CLICKED / OPENED] |
| Failures: |
| [SENT_TO_GATEWAY] -------------------> [BOUNCED / FAILED] -----> [DLQ / TOKEN DEACTIVATED] |
+---------------------------------------------------------------------------------------------------+
Ingestion of Carrier Webhooks:
- Downstream carriers dispatch HTTP POST callbacks to our
https://webhooks.example.com/carrier/status. - A high-throughput gateway validates the cryptographic signature (HMAC-SHA256) of the webhook.
- The event is written directly to Kafka topic
notification-delivery-receipts. - Consumer workers update the message state in Cassandra:
UPDATE notification_events SET status = 'DELIVERED', carrier_latency_ms = 412, delivered_at = toTimestamp(now()) WHERE message_id = 'msg_Twilio_1761899120'; - If the webhook reports
BadDeviceToken(APNs HTTP 410) orHardBounce(SES 550), the system dispatches an event to the User Device Service to deactivate the invalid token immediately.
9. Failure Modes, Edge Cases & Threat Modeling
| Failure Mode | Root Cause | Engineering Mitigation |
|---|---|---|
| Marketing Notification Storm | Marketing blast triggers massive queue backlog. | Physical queue partitioning: (OTP) queue runs on isolated worker pools with guaranteed CPU/network reservations. |
| Duplicate Billing / Double SMS | Worker crashes after sending SMS but before Kafka offset commit. | Distributed idempotency lease via Redis SETNX with a 10-minute TTL; duplicates drop immediately. |
| Provider Throttling () | Worker pool dispatches faster than vendor TPS quota. | Redis Lua Token Bucket rate limiter; on 429 response, apply exponential backoff with full randomized jitter. |
| Carrier Outage / Fiber Cut | Primary telecom aggregator experiences upstream downtime. | Automated three-state Circuit Breaker routes traffic to secondary carrier within 5 failed attempts. |
| Dead Device Token Churn | Users uninstall app; APNs returns 410 Gone. | Webhook reconciliation pipeline captures APNs/FCM invalidations and marks device tokens inactive in the user DB. |
10. Knowledge Check
Review the core architectural principles, then complete the 4-question interactive assessment quiz at the end of this lesson:
- Queue Isolation: Articulate why Head-of-Line blocking in shared queues destroys transactional OTP authentication SLAs.
- Idempotency Leases: Explain how Redis atomic
SETNXprevents duplicate SMS transmissions during Kafka consumer rebalances. - Token Bucket Math: Derive how burst capacity and replenishment rate shape traffic to match downstream vendor contracts.
- Circuit Breaker States: Trace the state transitions from
CLOSEDtoOPENtoHALF_OPENduring carrier degradation.
11. Practical Exercises & Deliverables
Exercise 1: Implement Redis Lua Token Bucket
Problem: Traditional client-side rate limiting fails in distributed clusters because 50 independent worker pods cannot synchronize local token counters without race conditions.
Deliverable:
- Write an atomic Redis Lua script implementing the Token Bucket algorithm (
KEYS[1]= bucket key,ARGV[1]= capacity,ARGV[2]= refill rate per sec,ARGV[3]= current timestamp). - Benchmark the script using
ioredisunder 5,000 concurrent requests. Prove that the total allowed requests never exceed the specified capacity and replenishment rate.
Exercise 2: Simulate Carrier Degradation & Circuit Breaker Failover
Problem: Test how your notification pipeline responds when the primary SMS vendor fails.
Deliverable:
- Create a mock vendor client that injects simulated network timeouts and HTTP 503 errors on 25% of requests.
- Wrap the vendor client in the three-state Circuit Breaker implemented in Section 7.
- Verify that after 5 consecutive failures, the breaker trips to
OPEN, routing 100% of subsequent dispatches to the secondary vendor with failover latency.
View Reference Architecture & Worked Solutions (APP-A02-001)
1. Reference Solution — Exercise 1: Implement Redis Lua Token Bucket
- Atomic Redis Lua Script (
token_bucket.lua):-- KEYS[1]: rate limiter bucket key (e.g., "ratelimit:twilio:sms") -- ARGV[1]: burst capacity b (e.g., 500) -- ARGV[2]: refill rate r in tokens/sec (e.g., 200) -- ARGV[3]: current timestamp in milliseconds -- ARGV[4]: requested tokens (e.g., 1) local key = KEYS[1] local capacity = tonumber(ARGV[1]) local refill_rate = tonumber(ARGV[2]) local now_ms = tonumber(ARGV[3]) local requested = tonumber(ARGV[4]) local state = redis.call('HMGET', key, 'tokens', 'last_ms') local tokens = tonumber(state[1]) local last_ms = tonumber(state[2]) if tokens == nil then tokens = capacity last_ms = now_ms else local elapsed_sec = math.max(0, (now_ms - last_ms) / 1000.0) tokens = math.min(capacity, tokens + elapsed_sec * refill_rate) last_ms = now_ms end if tokens >= requested then tokens = tokens - requested redis.call('HMSET', key, 'tokens', tokens, 'last_ms', last_ms) redis.call('PEXPIRE', key, math.ceil((capacity / refill_rate) * 2000)) return {1, math.floor(tokens)} -- Allowed else redis.call('HMSET', key, 'tokens', tokens, 'last_ms', last_ms) return {0, math.floor(tokens)} -- Throttled (HTTP 429) end - 5,000-Request Concurrency Proof:
- Because Redis executes each
EVALSHAinvocation single-threaded and atomically, 50 worker pods firing concurrent requests over a window against a bucket with and can acquire at most: while the remaining requests deterministically receive{0, 0}(THROTTLED) and reschedule with Full Jitter backoff.
- Because Redis executes each
2. Reference Solution — Exercise 2: Simulate Carrier Degradation & Circuit Breaker Failover
- Failover State Machine Verification:
- Wrap the primary carrier (
TwilioAdapter) and secondary carrier (InfobipAdapter) withCarrierCircuitBreaker(failureThreshold = 5, recoveryTimeMs = 30_000). - When
TwilioAdapterencounters 5 consecutiveHTTP 503or socket timeout errors,primaryBreaker.recordFailure()incrementsfailureCountto5and transitionsstatefrom'CLOSED'to'OPEN'. - On the 6th notification and all subsequent requests during the cooldown window,
primaryBreaker.canExecute()returnsfalsein (in-memory check, zero network wait), immediately routing of traffic toInfobipAdapter. - Crucially, if a request is throttled or fails before external acceptance, the dispatcher releases or withholds the
dedup:key so re-queued messages are delivered cleanly via the secondary carrier rather than suppressed as false duplicates.
- Wrap the primary carrier (
12. Summary & Architectural Decision Guide
- Physically Partition Priority Tiers: Never multiplex authentication OTPs and promotional campaigns on the same broker queue or worker pool; physical topic and consumer isolation prevents multi-million-message Head-of-Line blocking.
- Pair At-Least-Once Queues with Atomic Idempotency Leases: Acquire a deterministic Redis
SET ... NX EXdeduplication lease before invoking external vendor APIs so consumer crashes and Kafka rebalances never double-send SMS or email messages. - Shape Cluster Egress with Token Buckets and Full Jitter: Enforce contractual provider TPS limits via centralized Redis Lua Token Buckets, and handle transient
HTTP 429/503responses with Full-Jitter exponential backoff. - Close the Loop via Signed Webhooks: Treat
HTTP 202 Acceptedas handoff, not delivery; ingest HMAC-verified carrier callbacks asynchronously to reconcile terminal states and prune HTTP 410BadDeviceTokenregistrations.
13. Authoritative References & Version Context
- Apple Developer Documentation — Apple Push Notification service (APNs) HTTP/2 Provider API: Sending Notification Requests to APNs (HTTP/2 multiplexed provider protocol and
410 Unregisteredtoken lifecycle semantics). - Google Firebase Cloud Messaging (FCM) — HTTP v1 API Specification: FCM Architectural Overview & HTTP v1 Protocol (FCM v1 OAuth2 dispatch, topic/token fan-out, and quota error handling).
- Marc Brooker (AWS Builders’ Library) — Exponential Backoff and Jitter: Timeouts, Retries, and Backoff with Jitter (mathematical analysis of Full Jitter and Decorrelated Jitter under downstream contention).
- IETF RFC 6585 — Additional HTTP Status Codes (
429 Too Many Requests): RFC 6585 Section 4 (Retry-Aftersemantics and rate-limiting response contracts; baseline stack context: Apache Kafka 3.7+, Redis 7.2+, Apache Cassandra 4.1+/5.0).