Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 30

High-Level Architecture: Bottlenecks & Scaling

Recommended resources

Introduction

Welcome to the final lesson in our module on designing a notification system. In the previous lessons, we've done the essential groundwork: first, by defining the API contracts and a flexible data model, and second, by architecting the core asynchronous workflow using message queues to handle fan-out, retries, and failures.

Today, we will assemble all those pieces into a single, cohesive high-level architecture. This is the "money shot" for a system design interview—the complete diagram that shows how everything connects. However, drawing the diagram is only half the battle. A senior engineer is expected to analyze it, identify its weaknesses, and articulate clear strategies for scaling and resilience.

This lesson will focus on that analytical mindset. By the end, you'll be able to sketch the complete high-level architecture, integrating all components, and discuss potential bottlenecks and scaling strategies. This will prepare you not just to present a design, but to confidently defend and evolve it under pressure.


The Complete High-Level Architecture

Let's integrate all the components we've discussed into a final architectural diagram. This diagram represents a robust, scalable, and resilient notification system suitable for production.

High-Level Architecture of a Multi-Channel Notification System
A complete high-level architecture for a multi-channel notification system. This diagram shows the flow from the API Gateway, through the Notification Service, into queues, and out to workers that contact third-party service providers. It also includes critical resilience components like the Retry Service and Dead Letter Queue.

Let's walk through the end-to-end flow shown in the diagram:

  1. Ingestion: A client (another microservice, a script, or a frontend) sends a request to the API Gateway. The Gateway handles cross-cutting concerns like authentication and initial rate limiting before forwarding the request to the Notification Service.
  2. Processing & Fan-Out: The Notification Service validates the request, persists a record of the notification, and publishes a generic event to a Message Queue (e.g., a Kafka topic called notification_requests).
  3. Dispatching: A pool of Dispatcher services (not explicitly shown in this diagram but discussed in our previous lesson) consumes these events. For each event, it fetches user preferences from a Cache (or DB), determines the required channels (e.g., Email, SMS), and publishes channel-specific messages to dedicated queues (e.g., email_queue, sms_queue).
  4. Delivery: Stateless Notification Workers, scaled independently for each channel, consume messages from their respective queues. A worker's job is to call the appropriate third-party Service Provider (e.g., SendGrid for email, Twilio for SMS).
  5. Failure Handling:
    • If a delivery attempt fails due to a transient error, the message is sent to a Retry Queue. A Retry Service re-enqueues the message back into the main channel queue after an exponential backoff delay.
    • If a message fails after all retries, it is moved to a Dead Letter Queue (DLQ) for manual inspection, and an alert is triggered.
  6. Observability: An Analytics & Reporting Service subscribes to events from various parts of the system (e.g., successful sends, failures) to provide monitoring and insights.

To see this entire flow explained in a walkthrough format, let's watch the concluding section of the "Design A Scalable Notification System" video.

Design A Scalable Notification System | System Design

This segment provides a concise summary of the entire system, walking through the flow from start to finish. It will help solidify your understanding of how all the components interact.

Watch the section from 24:24 to 27:40. The narrator recaps the whole architecture, reinforcing the role of each component we've discussed.


Identifying Bottlenecks and Scaling Strategies

Your diagram is now on the whiteboard. The interviewer says, "This looks good for a million notifications a day. What happens when we need to send 100 million? Where will this break, and how would you fix it?"

This is where you demonstrate senior-level thinking. Let's break it down component by component.

1. The Ingestion Layer

  • Bottleneck: The Notification Service API becomes a throughput bottleneck during traffic spikes (e.g., a flash sale announcement). A single instance can only handle so many concurrent requests.
  • Scaling Strategy: Horizontal Scaling. Since the service is stateless (it offloads state to the database and message queue), you can run multiple instances behind a load balancer. This distributes the incoming traffic and increases overall throughput.

2. The Database

  • Bottleneck 1: Read-Heavy Load: The Dispatcher service needs to fetch user preferences and notification templates for every single notification. This can hammer your primary database with repetitive read queries.
  • Scaling Strategy 1: Caching: Introduce a distributed cache like Redis. Store frequently accessed user preferences and templates in the cache. This drastically reduces database load. The Dispatcher checks the cache first; if the data isn't there (a "cache miss"), it queries the database and then populates the cache for subsequent requests (cache-aside pattern).

How to Design a Notification System: A Complete Guide

The 'System Design Handbook' guide offers a great tip on this exact problem. Let's review it.

Read the 'Tip' at the end of the 'User preferences and personalization' section, and then read the 'Scaling and reliability strategies' section. Note the explicit recommendation to cache user preferences and the mention of database partitioning (sharding).

  • Bottleneck 2: Write-Heavy Load: Every notification event might generate multiple writes: creating the initial notification record, tracking its status through pending -> sent -> delivered, and logging retry attempts. At 100 million notifications/day, this is thousands of writes per second, which can overwhelm a traditional relational database like PostgreSQL.
  • Scaling Strategy 2: Sharding & NoSQL:
    • Sharding: Partition your relational database. For example, you could shard the notifications table by user_id or tenant_id. This distributes the data and write load across multiple database instances.
    • NoSQL for Status Tracking: For the high-volume, high-write-velocity task of tracking notification status, consider a NoSQL database optimized for writes, like Apache Cassandra or AWS DynamoDB. You could store the "static" information (template, channel config) in PostgreSQL and the "dynamic" status updates in Cassandra, referencing by a shared notification_id.

3. The Workers and Third-Party APIs

  • Bottleneck: The delivery speed of workers is limited by the latency of external service providers (e.g., APNS, Twilio). If a provider is slow, your workers get stuck waiting, and the queue depth grows.
  • Scaling Strategy 1: Independent Auto-Scaling: This is a crucial pattern. Configure your worker pools to auto-scale horizontally based on queue depth.
    • Example: Set a rule that if the sms_queue has more than 1,000 messages waiting, spin up 5 new SMS Worker instances. If the queue depth drops below 100, scale them back down to save costs. This allows the system to elastically respond to channel-specific load spikes.
  • Bottleneck 2: Unreliable Third-Party Providers: A provider can go down completely or start returning high error rates. Continuously retrying against a dead service wastes resources and blocks workers.
  • Scaling Strategy 2: The Circuit Breaker Pattern:
    • Implement a circuit breaker for each external provider. If the error rate for Twilio exceeds a threshold (e.g., 20% in one minute), the circuit "opens." For the next few minutes, all attempts to send an SMS via Twilio will fail instantly without making a network call. This gives Twilio time to recover and frees up your workers. After a timeout, the circuit moves to a "half-open" state, allowing a few test requests through. If they succeed, the circuit closes; if not, it remains open.

The Art of Notifying: A System Design Journey

The article from Ahmed Safwat provides a fantastic, interview-style discussion of the entire design process, including these advanced patterns. It also covers observability, which is our next topic.

First, read the 'Further Extensions and Alternatives' section. Focus on the descriptions of the Circuit Breaker Pattern and Prioritized Queues. Then, read the section 'Now let’s talk about observability'. This is a critical piece for demonstrating a production mindset.


Making It Production-Ready: Observability

A system design is incomplete without a plan for monitoring its health. When the support team says, "Users are complaining they aren't receiving password reset emails," how do you debug it? You need observability. The article you just read introduced the three pillars:

  1. Structured Logging with Correlation IDs: When a request first enters your Notification Service, generate a unique correlation_id. This ID must be passed along in every log message, every queue event, and every subsequent service call related to that original request. This allows you to search your logs (e.g., in Splunk or an ELK stack) for that single ID and get a complete, ordered trace of one notification's journey across five different microservices.

  2. Metrics and Dashboards: You need to measure and visualize key performance indicators (KPIs) in real-time using tools like Prometheus and Grafana. Essential metrics include:

    • Throughput: notifications_sent_per_second (by channel)
    • Latency: p99_delivery_time (from ingestion to final delivery confirmation)
    • Queue Health: queue_depth (by channel)
    • Error Rate: delivery_failures_per_minute (by provider and error type)
  3. Alerting: Based on these metrics, you set up automated alerts. For example: ALERT if email_queue_depth > 10,000 for 5 minutes or ALERT if twilio_error_rate > 15%. This allows your on-call team to proactively address issues before they become widespread outages.


Conclusion

In this module, we've designed a complete notification system, from its public-facing API to its deep, internal workings. We didn't just create a static diagram; we built a dynamic system capable of handling failures, scaling under load, and providing the visibility needed to operate it in production.

Key Takeaways:

  • The Complete Picture: A robust notification system is a decoupled, asynchronous pipeline involving an ingestion service, message queues, dispatchers, and channel-specific workers.
  • Scale Proactively: Identify potential bottlenecks (API, database, workers) and have clear scaling strategies for each, such as horizontal scaling, caching, sharding, and auto-scaling workers based on queue depth.
  • Design for Failure: Assume external services will fail. Use retries with exponential backoff for transient errors, a Dead Letter Queue for permanent failures, and the Circuit Breaker pattern to protect your system from failing dependencies.
  • Operate What You Build: A production-ready design must be observable. Implement structured logging, comprehensive metrics, and automated alerting to ensure you can debug and maintain the system's health.

Preview of the Next Module:

We've now established a strong foundation in asynchronous system design. Many of the patterns we've used—message queues, workers, retries, DLQs—are fundamental building blocks for many backend systems. In the next module, "Applying System Design: Background Job Processor," we will leverage these same concepts to design another common and critical backend system, reinforcing your understanding and ability to apply these patterns to new problems.