Answer A: Anthropic Claude Opus 5
REAL-TIME NOTIFICATION SERVICE — SYSTEM DESIGN PLAN
- REQUIREMENTS RECAP AND CAPACITY ESTIMATION
Traffic math:
- 100M DAU x 10 events/day = 1B notification-triggering events per day.
- Average = 1B / 86,400 ≈ 11,600 events/sec. Peak at 5x ≈ 58,000 events/sec.
- Fan-out multiplier: most events (like, comment, follow, DM) target exactly one recipient, so fan-out is ~1:1. However, events from high-follower accounts (a celebrity posting) can fan out to millions of recipients; treat these as a separate "bulk fan-out" path. Assume effective peak delivery of 80,000–100,000 notifications/sec including bulk campaigns....
Show Full Answer ▼
REAL-TIME NOTIFICATION SERVICE — SYSTEM DESIGN PLAN
- REQUIREMENTS RECAP AND CAPACITY ESTIMATION
Traffic math:
- 100M DAU x 10 events/day = 1B notification-triggering events per day.
- Average = 1B / 86,400 ≈ 11,600 events/sec. Peak at 5x ≈ 58,000 events/sec.
- Fan-out multiplier: most events (like, comment, follow, DM) target exactly one recipient, so fan-out is ~1:1. However, events from high-follower accounts (a celebrity posting) can fan out to millions of recipients; treat these as a separate "bulk fan-out" path. Assume effective peak delivery of 80,000–100,000 notifications/sec including bulk campaigns.
- Storage: history of last 100 notifications per user. Assume ~400 bytes per row -> 100 x 400B = 40KB/user -> 100M users ≈ 4TB of hot history, plus replication factor 3 ≈ 12TB. Trivially shardable.
- Connections: assume 20–25% of DAU concurrently online at peak ≈ 20–25M live WebSocket connections. With ~100K connections per gateway node (tuned Linux, epoll), that is 200–250 nodes plus headroom.
Non-functional targets:
- End-to-end p99 latency under 2s for online users (measured from event ingestion to client ack).
- 99.9% availability = ~43 minutes downtime per month.
- At-least-once delivery with client/server-side idempotent deduplication.
- HIGH-LEVEL ARCHITECTURE
The design is an event-driven pipeline with a clear split between the ingestion path (fast, durable, write-optimized) and the delivery path (channel-specific, retryable).
Flow:
Producer services (Post Service, Social Graph Service, Comment Service, Messaging Service)
-> Notification API / Gateway (gRPC + REST, auth, rate limiting, schema validation, idempotency key)
-> Kafka topic notification.events (partitioned by recipient_user_id where known, else by actor_id)
-> Fan-out Service (resolves recipients, expands celebrity events, applies aggregation rules)
-> Kafka topic notification.deliveries (one message per recipient)
-> Preference & Policy Service (in-line lookup via cache: channel opt-ins, quiet hours, mute/block, digest vs instant)
-> Router / Dispatcher writes to per-channel topics:
deliver.websocket, deliver.mobile_push, deliver.web_push, deliver.email
-> Channel Workers:
WebSocket Dispatcher -> Connection Registry lookup -> Realtime Gateway node -> client
Mobile Push Worker -> APNs (iOS) / FCM (Android)
Web Push Worker -> VAPID/Web Push protocol via browser push services
Email Worker -> SES / SendGrid, with template rendering and digest batching
-> In parallel, a History Writer consumer persists every delivery to the Notification History store and increments unread counters.
Cross-cutting: Template Service, Device Registry, Connection Registry, Dedup Store, Dead Letter Queues, Observability stack, and an Admin/Analytics plane consuming the same Kafka streams.
- KEY COMPONENTS AND RESPONSIBILITIES
Notification API Gateway
- Single entry point for internal producers (gRPC, protobuf schemas in a registry) and for client read APIs (REST/GraphQL for history, mark-as-read, preferences).
- Responsibilities: authentication (mTLS service-to-service, JWT for clients), per-tenant and per-producer rate limiting/quotas, request validation, idempotency key acceptance, and immediate durable append to Kafka. It returns 202 Accepted quickly; it does no delivery work inline.
Event Ingestion / Kafka
- Durable, replayable log.
notification.eventswith ~256 partitions, replication factor 3, min.insync.replicas=2, acks=all, retention 7 days for replay and incident recovery. - Provides natural back-pressure and buffering for the 5x peak, decoupling producers from slow third-party providers.
Fan-out Service
- Resolves an event into a recipient list. For 1:1 events, it is a pass-through. For 1:N events (new post to followers, group thread activity), it pages the Social Graph Service and emits recipient messages in batches.
- Hybrid fan-out strategy: push-based (write per recipient) for normal accounts; pull-based/lazy for accounts above a follower threshold (e.g., 1M+), where a single "feed marker" is written and recipients materialize on read. This prevents a celebrity event from creating a 50M-message write storm on the hot path.
- Rate-limits bulk expansions onto a separate low-priority Kafka topic so they never starve interactive notifications.
- Applies aggregation/coalescing: "Alice and 24 others liked your post" instead of 25 rows, using a short tumbling window (e.g., 30–60s) keyed by (recipient, post_id, type) in Redis.
Preference & Policy Service
- Stores and serves per-user channel preferences, category-level opt-ins, quiet hours with timezone, device-level settings, mute/block lists, and legal consent flags (GDPR/CAN-SPAM).
- Read path is cached in Redis with write-through invalidation; p99 lookup target under 5ms. Source of truth in a relational store.
- Enforces frequency capping (e.g., max N pushes/hour per user) to protect user experience and provider quotas.
Router / Dispatcher
- Maps an approved delivery into channel-specific work items. Decides "in-app + WebSocket only if online; otherwise mobile push" using the Connection Registry, and schedules email digests instead of instant sends when preferences say so.
Realtime Gateway (WebSocket layer)
- Stateful tier holding persistent WebSocket connections (fallback: Server-Sent Events, then long polling).
- On connect: authenticate, register (user_id, device_id) -> gateway_node_id in the Connection Registry (Redis Cluster, TTL + heartbeat refresh), then push any unread backlog.
- On delivery: dispatcher looks up node, sends over an internal gRPC stream or a per-node Kafka/Redis Pub/Sub channel; the gateway writes to the socket and waits for a client ack.
- Heartbeats every 30s; missed heartbeats evict the registry entry and future notifications automatically fall back to mobile push.
Channel Workers
- Mobile Push Worker: batches to APNs (HTTP/2 multiplexed, token-based auth) and FCM. Handles per-provider concurrency limits, exponential backoff with jitter, and prunes invalid/unregistered device tokens from the Device Registry.
- Web Push Worker: Web Push protocol with VAPID keys and payload encryption.
- Email Worker: renders templates, supports instant, hourly, and daily digests; handles bounces/complaints via provider webhooks and maintains a suppression list.
- All workers are idempotent consumers keyed by notification_id and write terminal status back to a
delivery.statustopic.
History Writer & Read Path
- Consumer that persists notifications and maintains unread counters in Redis (authoritative counter periodically reconciled from the store).
- Read API serves the last 100 notifications from the history store, with a Redis cache of the first page per user for sub-10ms typical reads.
Supporting services
- Template Service: versioned, localized templates with variable interpolation; decouples copy changes from code deploys.
- Device Registry: device_token, platform, app_version, locale, timezone, last_seen.
- Dedup Store: Redis with a 24h TTL on (producer_id, idempotency_key) to make at-least-once behave as effectively-once.
- Scheduler: for delayed/scheduled notifications and digest windows (time-bucketed queues in Redis sorted sets or a dedicated scheduler like a Kafka delay topic ladder).
- DLQ + Replayer: poison messages parked and replayable after fixes.
- DATA MODEL AND DATABASE CHOICES
a) Notification History — Cassandra (or ScyllaDB / DynamoDB)
Table: notifications_by_user
- Partition key: user_id
- Clustering key: created_at DESC, notification_id
- Columns: type, actor_id(s), target_type, target_id, aggregated_count, preview_text, image_url, deep_link, read_at, channels_sent, created_at
- TTL: 90 days; application caps reads at 100 rows.
Justification: the access pattern is a single, well-known partition key with a time-ordered slice — exactly Cassandra's sweet spot. It offers linear write scalability (we need 60–100K writes/sec sustained), multi-region masterless replication for availability, tunable consistency (write QUORUM/LOCAL_QUORUM, read LOCAL_ONE for the feed), and native TTL for retention. We do not need joins or multi-row transactions here, so a relational store would only add sharding pain and write-amplification risk.
b) Unread counters and hot first page — Redis Cluster
unread:{user_id}integer counter;notif:page0:{user_id}cached JSON list.
Justification: counters are read on every app open (extremely high QPS, low value per read) and must be single-digit-millisecond. Redis absorbs this cheaply; Cassandra counters are comparatively costly and error-prone. Counters are reconciled asynchronously so drift self-heals.
c) User preferences and device tokens — PostgreSQL (sharded by user_id) with Redis read-through cache
Tables: user_preferences(user_id, category, channel, enabled, quiet_hours_start, quiet_hours_end, timezone, updated_at), devices(device_id, user_id, platform, push_token, locale, last_seen, active), suppression_list(email, reason, created_at).
Justification: this data is low-volume, read-heavy, relational (users -> devices -> per-category settings), and benefits from transactions and constraints for correctness and auditability (consent records have compliance weight). Volume is small enough (~100M rows) to shard trivially and to cache almost entirely.
d) Connection Registry — Redis Cluster
conn:{user_id}-> set of {device_id, gateway_node_id, connected_at}, TTL 90s refreshed by heartbeat.
Justification: ephemeral, extremely high churn, must be fast; durability is unnecessary because a lost entry degrades gracefully to push fallback.
e) Dedup / idempotency — Redis with TTL, backed by nothing (loss only risks a rare duplicate).
f) Templates and config — PostgreSQL + object storage for assets, cached at the edge.
g) Analytics — events streamed from Kafka to a data lake (S3/Parquet) and warehouse (Snowflake/BigQuery) for delivery-rate, open-rate, and latency reporting; ClickHouse for near-real-time operational dashboards.
- TECHNOLOGY STACK RECOMMENDATIONS
- Language/runtime: Go for the gateway, realtime gateway, and workers (goroutines and low memory per connection suit millions of sockets); Java/Kotlin acceptable for Kafka Streams-heavy aggregation.
- Messaging: Apache Kafka (managed: MSK/Confluent) as the backbone; separate topics per channel and per priority class. Kafka Streams or Flink for windowed aggregation/coalescing.
- Realtime transport: WebSocket over TLS with SSE and long-poll fallbacks; NLB/L4 load balancing with connection draining; sticky routing not required because the registry stores node identity.
- Caching: Redis Cluster (or Elasticache/MemoryDB) for counters, preferences, registry, dedup, rate limits.
- Datastores: Cassandra/ScyllaDB for history; PostgreSQL (Aurora) for preferences/devices; S3 + warehouse for analytics.
- Push providers: APNs, FCM, Web Push (VAPID); email via SES with SendGrid as a secondary provider behind a provider-abstraction layer for failover.
- Infrastructure: Kubernetes with HPA/KEDA scaling on Kafka consumer lag (not just CPU), Envoy/Istio service mesh for mTLS and retries, Terraform for IaC.
- Observability: OpenTelemetry tracing (trace id propagated from producer event to client ack), Prometheus + Grafana metrics, structured logs in Loki/ELK, PagerDuty alerting on SLOs.
- Resilience libraries: circuit breakers and bulkheads per external provider, token-bucket rate limiters, exponential backoff with jitter.
- SCALABILITY, LATENCY, AND AVAILABILITY STRATEGIES
Scalability
- Every stateless component (API, fan-out, router, workers) scales horizontally; Kafka partition count is the parallelism ceiling, so provision partitions for 5–10x current peak from day one (repartitioning is operationally painful).
- Autoscale on consumer lag with KEDA so a 5x spike triggers scale-out within seconds; keep a warm buffer of 30–40% headroom because scaling is not instantaneous.
- Shard by user_id consistently across Cassandra, Postgres, and Redis so a single user's data is co-located and hot-partition mitigation is uniform.
- Priority lanes: interactive notifications (DM, comment on your post) use a high-priority topic with dedicated consumer groups; bulk/marketing/celebrity fan-out uses a throttled low-priority lane. This guarantees the 2s SLO for the notifications users actually notice.
- Celebrity/hot-key handling via the hybrid push/pull fan-out described above, plus salted partition keys for extremely hot targets.
Low latency (under 2s p99)
- The critical path is intentionally short: API append -> Kafka -> fan-out -> preference cache hit -> registry lookup -> WebSocket write. All lookups are Redis (sub-5ms); no synchronous database writes block delivery.
- History persistence and analytics are asynchronous, off the delivery path.
- Kafka producers tuned with linger.ms=5 and compression (lz4) to balance batching against latency; consumers use manual commit after processing.
- Multi-region deployment with users pinned to the nearest region to cut RTT; WebSocket connections terminate at regional edges.
- Aggregation windows are configurable per type and disabled for latency-critical types like DMs.
- Continuous synthetic probes measure true end-to-end latency per region and per channel.
High availability (99.9%+)
- No single points of failure: multi-AZ for every tier, Kafka RF=3 with min.insync.replicas=2, Cassandra RF=3 with LOCAL_QUORUM writes, Postgres with synchronous standby and automated failover.
- Active-active multi-region for the realtime and delivery tiers; Cassandra replicates cross-region asynchronously, Postgres uses regional read replicas with a designated write region.
- Graceful degradation ladder: if the WebSocket path is unhealthy, fall back to mobile push; if the preference cache is down, fall back to Postgres and then to conservative defaults; if Cassandra writes fail, keep delivering in real time and replay history writes from Kafka afterward.
- Retries with exponential backoff plus jitter, capped attempts, then DLQ with alerting and a replay tool.
- Circuit breakers per external provider so an APNs outage cannot exhaust worker threads and stall email.
- At-least-once delivery plus notification_id dedup on server and client; clients also dedup on reconnect when replaying the unread backlog.
- Reliability practices: chaos/game-day exercises (kill a gateway node and verify push fallback), load tests at 5x peak, blue-green and canary deploys, feature flags for per-channel kill switches, backpressure shedding of low-priority traffic before high-priority.
- BOTTLENECKS AND TRADE-OFFS
Likely bottlenecks
- Third-party providers (APNs/FCM/email): the hardest ceiling, since throughput is not ours to scale. Mitigate with connection pooling over HTTP/2, batching, per-provider rate limiters, multi-provider failover for email, and queue-based smoothing of spikes.
- Celebrity fan-out: a single post can generate tens of millions of deliveries. Mitigated by hybrid push/pull, throttled bulk lanes, and aggregation.
- WebSocket connection tier: memory and file descriptors per node, plus thundering-herd reconnects after a deploy or network blip. Mitigate with tuned kernel limits, staggered reconnect with jitter and exponential backoff enforced client-side, and slow connection draining during deploys.
- Redis hot keys: a viral post's aggregation key or a shared counter. Mitigate with key salting, local in-process caches with short TTLs, and client-side sharding.
- Cassandra write amplification and compaction pressure at 100K writes/sec. Mitigate with time-window compaction (TWCS) suited to time-series TTL data, and by not storing low-value notification types in history at all.
- Kafka partition skew if partitioning by a low-cardinality key; always partition by recipient_user_id on the delivery topic.
Explicit trade-offs
- At-least-once over exactly-once: exactly-once across heterogeneous external providers is impractical and costly. We accept rare duplicates and solve them with idempotency keys and client-side dedup — cheaper and far more available.
- Eventual consistency for counters and history over strong consistency: an unread badge that is momentarily off by one is acceptable; multi-second latency to guarantee it is not. Reconciliation jobs bound the drift.
- Polyglot persistence over a single database: this adds operational surface (three datastores plus Redis) but each workload gets the right engine. A single Postgres cluster would become the bottleneck at 1B writes/day; a single Cassandra cluster would make preferences and consent auditing awkward.
- Push-based fan-out for most users, pull-based for celebrities: added code complexity, but it is the only way to avoid unbounded write storms.
- Aggregation improves user experience and cuts delivery volume dramatically, but adds up to a window's worth of latency for aggregatable types. We keep windows short and exempt DMs.
- Multi-region active-active raises cost and introduces cross-region consistency subtleties, justified by the availability and latency targets. If budget were constrained, a single-region-with-warm-standby posture would still meet 99.9% but with a longer RTO.
- Kafka over a simpler managed queue (SQS): more operational burden, but we need replay, ordered per-key partitions, multiple independent consumer groups, and 7-day retention for incident recovery.
- Storing only the last ~100 notifications with a 90-day TTL trades archival completeness for cost and read performance; long-term data lives in the analytics lake if ever needed.
- ROLLOUT PLAN (PHASED)
Phase 1: Ingestion API + Kafka + history writer + read API (in-app notifications only). Establishes the durable backbone.
Phase 2: Realtime gateway, connection registry, unread counters, WebSocket delivery with push fallback.
Phase 3: Mobile push and web push workers, device registry, preference service and quiet hours.
Phase 4: Email instant + digests, template service, suppression handling.
Phase 5: Aggregation/coalescing, hybrid celebrity fan-out, frequency capping.
Phase 6: Multi-region active-active, chaos testing, 5x load validation, SLO dashboards and error budgets.
- KEY METRICS AND SLOs
- End-to-end p50/p95/p99 latency per channel (SLO: p99 under 2s for WebSocket).
- Delivery success rate per channel; provider error rates and token invalidation rate.
- Kafka consumer lag per topic (primary autoscaling and paging signal).
- WebSocket connection count, churn rate, and ack rate.
- Unread counter drift detected by reconciliation.
- DLQ depth and age.
- Availability per API measured from the client's perspective, tracked against a monthly error budget.
Result
Winning Votes
3 / 3
Average Score
Total Score
Overall Comments
Answer A is an outstanding, highly concrete design with quantified capacity estimates, clearly separated ingestion and delivery paths, detailed data models, priority lanes, hybrid fan-out, channel-specific resilience, and actionable availability and latency measures. Its strongest features are the explicit operational settings, WebSocket capacity planning, failure-degradation paths, and unusually thorough bottleneck and trade-off analysis. The main weakness is that it does not explicitly use an outbox or transactional event-publication pattern at source services, leaving a potential gap between the originating business transaction and durable Kafka ingestion. Some multi-region consistency details are also treated at a high level.
View Score Details ▼
Architecture Quality
Weight 30%The durable event-driven pipeline, separate fan-out and channel topics, policy routing, connection registry, realtime gateway, asynchronous history writer, and priority lanes form a logical architecture with clear responsibilities. The principal gap is the lack of an explicit source-service outbox, so a business transaction could theoretically commit without its notification event reaching the ingestion API. The relationship between one logical history record and multiple channel deliveries could also be stated more precisely.
Completeness
Weight 20%It addresses every requested area and goes beyond them with capacity estimates, connection sizing, detailed schemas, channel behavior, compliance controls, observability, rollout phases, and measurable SLOs. Minor omissions include source transactional publication and a more exact explanation of how the store enforces a strict last-100 policy rather than merely limiting reads and applying TTL.
Trade-off Reasoning
Weight 20%The answer explicitly and correctly examines at-least-once versus exactly-once delivery, eventual versus strong consistency, polyglot persistence, push versus pull fan-out, aggregation latency, multi-region cost, Kafka versus simpler queues, and retention limits. The trade-offs are tied to requirements and accompanied by mitigation strategies rather than being listed abstractly.
Scalability & Reliability
Weight 20%The design quantifies average and peak traffic, estimates concurrent WebSocket capacity, scales consumers by Kafka lag, reserves warm headroom, isolates priority traffic, handles celebrity fan-out, and specifies multi-AZ replication, quorum settings, retries, DLQs, circuit breakers, fallback paths, load tests, and chaos exercises. Some active-active regional data behavior and source-event atomicity require further detail.
Clarity
Weight 10%Despite its length, the numbered organization, explicit flow, named components, schemas, and separate sections for scaling, availability, bottlenecks, rollout, and metrics make the plan easy to navigate. A few statements are overly confident or compressed, such as calling 100M-row sharding trivial, and some channel/history semantics could be phrased more carefully.
Total Score
Overall Comments
An outstanding response that exemplifies a senior-level system design. It is comprehensive, well-structured, quantitatively grounded, and demonstrates a deep understanding of trade-offs and operational realities. The design is highly detailed, with specific technology choices and concrete strategies for scalability and reliability. The inclusion of a phased rollout plan and a dedicated metrics/SLOs section elevates it beyond a purely theoretical design, making it feel like a production-ready document.
View Score Details ▼
Architecture Quality
Weight 30%The architecture is exceptionally sound, detailed, and well-articulated. The event-driven flow is clear, and the separation of concerns between components like the API, Fan-out service, and channel-specific workers is excellent. The inclusion of a hybrid push/pull fan-out strategy for celebrity accounts demonstrates a sophisticated understanding of the problem space.
Completeness
Weight 20%This answer is exceptionally complete. It addresses every part of the prompt in great detail and goes above and beyond by including a detailed capacity estimation, a phased rollout plan, and a dedicated section for key metrics and SLOs. This level of detail is what one would expect from a senior engineer's design document.
Trade-off Reasoning
Weight 20%The discussion of bottlenecks and trade-offs is excellent. It not only identifies potential problems but also explicitly lists the design compromises made, such as choosing at-least-once delivery over exactly-once and using polyglot persistence. The reasoning is sharp, concise, and demonstrates a mature engineering perspective.
Scalability & Reliability
Weight 20%The strategies for scalability and reliability are both detailed and concrete. It mentions specific approaches like autoscaling on Kafka consumer lag with KEDA, using priority lanes for different traffic types, and implementing a graceful degradation ladder. The inclusion of practices like chaos engineering shows a proactive approach to reliability.
Clarity
Weight 10%The answer is exceptionally clear and well-structured. The use of numbered sections, a text-based flow diagram at the beginning, and concise bullet points makes the large amount of technical information very easy to follow and digest. The logical flow from requirements to metrics is impeccable.
Total Score
Overall Comments
Answer A is a near staff-level design document. It opens with concrete capacity math (events/sec, storage sizing, WebSocket connection counts and node estimates), then walks through a clean ingestion-vs-delivery split with specific configuration details (Kafka partition counts, replication settings, acks, retention), a hybrid push/pull fan-out for celebrity accounts, priority lanes to protect the 2s SLO, per-store data models with explicit justifications, a graceful degradation ladder, chaos/game-day practices, a phased rollout plan, and SLO metrics. The trade-off section is exceptional: each choice (at-least-once vs exactly-once, polyglot persistence, Kafka vs SQS, aggregation latency cost, multi-region cost) is stated with the alternative and why it was rejected. Minor weaknesses: density can make it heavy reading, and it does not discuss the outbox pattern for producer-side event durability.
View Score Details ▼
Architecture Quality
Weight 30%Outstanding architecture: clear ingestion/delivery split, hybrid push/pull fan-out with a concrete follower threshold, priority lanes guaranteeing the 2s SLO for interactive events, sized connection tier (20-25M sockets, ~100K per node), and specific Kafka topology (256 partitions, RF=3, min.insync.replicas=2, 7-day retention). Component responsibilities are precise and the critical path is deliberately kept free of synchronous DB writes.
Completeness
Weight 20%Covers every prompt requirement plus extras: capacity estimation with storage math, full data models for six stores with justifications, all four channels, digests, compliance (GDPR/CAN-SPAM, suppression lists), observability with SLOs, a phased rollout plan, and a dedicated bottlenecks/trade-offs section. Nothing from the prompt is missing.
Trade-off Reasoning
Weight 20%Exceptional trade-off section: each decision names the alternative and why it was rejected (at-least-once vs exactly-once, polyglot persistence vs single DB, Kafka vs SQS, push vs pull fan-out, active-active cost vs warm standby, aggregation latency vs volume). Trade-offs are woven into justifications throughout, not confined to one section.
Scalability & Reliability
Weight 20%Concrete and quantified: partition over-provisioning rationale, KEDA autoscaling on consumer lag with 30-40% warm headroom, salted hot keys, TWCS compaction for the write rate, explicit Cassandra/Postgres/Kafka replication and quorum settings, graceful degradation ladder, circuit breakers per provider, chaos testing and 5x load validation, and DLQ replay tooling. Directly ties mechanisms to the 100M DAU / 5x peak / 99.9% targets.
Clarity
Weight 10%Excellent numbered structure from requirements through rollout and metrics; the flow diagram in text form makes the pipeline easy to follow. The density of detail occasionally makes sections heavy, but headings and consistent formatting keep it navigable.