Understanding the Problem
A notification system is a shared internal service that allows other applications within a company to deliver messages to users through channels such as mobile push notifications, email, and SMS.
Instead of every product team building its own delivery infrastructure, internal services submit the recipient, message content, and delivery preferences to this platform. The notification system then handles the downstream work required to route and send the message through the appropriate channel.
Unlike a typical consumer-facing product, this is primarily an infrastructure platform whose direct users are other engineering teams. For example, an authentication service may use it to send a one-time verification code to a single user.
A commerce service might notify a customer that an order has shipped. At the other extreme, a growth or marketing system may submit a campaign that needs to reach millions of users within a short period of time.
The system therefore needs to support both small, latency-sensitive transactional notifications and very large bulk delivery workloads through the same underlying platform.
Functional Requirements
- Internal services can send a notification to an individual user through push, email, or SMS. The notification may be delivered immediately or scheduled for a later time.
- Internal services can create bulk campaigns that send the same notification to a defined group of users. Campaigns should support both immediate delivery and scheduled execution.
- Users can control how and when they receive notifications. This includes opting out of specific channels and configuring quiet hours during which non-urgent messages should be delayed.
Non-Functional Requirements
- The system should provide at-least-once delivery with best-effort duplicate suppression. If a failure forces us to choose, delivering the same notification twice is preferable to losing it completely.
- The platform should remain stable during bursts of roughly 3,000 notifications per second.
- High-priority traffic, such as one-time passwords and security warnings, should begin delivery within approximately 3 seconds of being accepted, even while lower-priority campaigns are creating heavy load.
Data Model
To support the functional requirements, we need four primary entities:
- Notification: Represents one message intended for one recipient over one delivery channel. It also tracks the notification’s current lifecycle state, such as scheduled, queued, sent, delivered, or failed. For example, an email promotion sent to a specific user at 4:00 PM.
- Campaign: Represents a bulk notification job targeting many users. It stores information such as the message content, chosen channel, scheduled send time, and the audience that should receive it.
- Segment: Represents a reusable group of users, such as “customers in Canada” or “users who purchased in the last 30 days.” Membership is typically managed by another audience or analytics system, while the notification platform reads that membership when expanding a campaign into individual deliveries.
- User: Represents the notification recipient. The user record includes delivery information such as email address, phone number, or device tokens, along with preferences like channel opt-outs and quiet-hour settings.
API Design
The first capability we need is sending a notification to one recipient. The request should include who the message is for, which delivery channel to use, how urgent it is, the message body, and optionally when it should be sent.
If no schedule is provided, we treat the notification as ready for immediate delivery.
POST /notifications Body: { userId, channel, // push | email | sms priority, // high | standard content, // { title, body } scheduledAt // optional, defaults to current time } Response: 200 OK { id, status }
POST is appropriate because the request creates a new Notification resource.
For the initial version of the design, we can assume that the notification service contacts the downstream provider directly while processing the request. Under that model, the API can return a synchronous success or failure because accepting the request and attempting delivery happen in the same flow.
Later, once we introduce asynchronous processing, those two events will become separate. The system may successfully accept a notification even though the actual email, SMS, or push delivery has not happened yet.
Bulk campaigns should have a separate API.
We could allow /notifications to accept a segmentId, but that would make one endpoint represent two very different resources: sometimes a single notification and sometimes a bulk campaign. Keeping them separate makes the API contract easier to reason about.
POST /campaigns Body: { segmentId, channel, // push | email | sms priority, // high | standard templateId, scheduledAt // optional, defaults to current time } Response: 202 Accepted { id }
A campaign returns 202 Accepted because creation only confirms that the platform has accepted the job. Expanding the audience and delivering the individual notifications happens asynchronously, so there is no final delivery result available at request time.
The distinction between 200 and 202 is unlikely to be the main focus of an interview, but using the correct semantics helps communicate how the execution model works.
We will also eventually add an idempotency key to both creation APIs. Clients may retry requests after timeouts or network failures, and we do not want those retries to create duplicate notifications or duplicate campaigns.
It is reasonable to begin with the simpler API and introduce idempotency once we discuss reliability, as long as we make that evolution explicit.
The third functional requirement concerns user preferences.
Because the client is replacing the current preference configuration for a known user, PUT is a natural choice. It is also inherently idempotent: submitting the same preference state multiple times produces the same result.
PUT /users/{userId}/preferences Body: { optOuts, // e.g. ["sms"] quietHours // e.g. { start: "22:00", end: "08:00", tz } } Response: Preferences
At first glance, placing userId directly in the URL may look suspicious. In many consumer-facing APIs, user identity should come from the authenticated session or JWT rather than from a client-provided path parameter.
This API has a different trust model.
The callers are internal backend services, not end users. Those services authenticate to the notification platform using service credentials and act on behalf of users whose identities they have already verified.
For example, when a customer disables SMS notifications from the product settings page, the product backend authenticates that user first. It then calls the notification platform with the corresponding userId and the updated preferences.
In this context, using userId in the resource path is intentional because the API is designed for trusted internal services managing notification settings on behalf of users.
High-level Design
Upstream Services Can Send Notifications Immediately or Schedule Them
We will start with the simplest case: one internal service wants to send one notification to one user immediately. To keep the first version focused, assume the notification is an email. SMS follows almost the same flow, while push notifications require one additional piece of device-specific state that we will add afterward.
Immediate Delivery
The initial architecture contains four main components.
API Gateway: The entry point for internal callers such as the authentication, commerce, or order services. It validates service credentials, applies per-client rate limits, and prevents a noisy integration from consuming capacity needed by more important workloads.
Notification Service: The central application layer. It resolves the recipient, creates the notification record, selects the appropriate delivery provider, and tracks the result of the send attempt.
Notification Database: Stores notification state together with the subset of recipient data required for delivery. We can use PostgreSQL here because the records are straightforward and do not require a specialized storage engine.
Email Provider: An external provider responsible for actually delivering the message to the recipient’s mailbox. The notification platform hands the provider a properly formed request through SMTP or an HTTP API, but it does not operate the mail infrastructure itself.
For email and SMS, we maintain a local copy of the user contact information required for delivery:
User - userId - name - email - phoneNumber - ...
This data can be synchronized from the company’s primary User Service rather than fetched remotely for every notification. A Notification record might contain: