Grokking the System Design Interview, Volume II
Vote

0% completed

Notification System: API Design

Step 4: API Specifications

Step 4: API Specifications

Four fields, four decisions. Channels and priority are the two that the rest of the design has to honour.
Four fields, four decisions. Channels and priority are the two that the rest of the design has to honour.

1. Send Notification

  • POST /v1/notifications
  • Headers: Authorization: Bearer <tenant-key>
  • Request Body:
{ "user_id": "u_12345", "template_id": "order_shipped_v1", "channels": ["push", "email"], "priority": "transactional", "idempotency_key": "uuid-gen-by-client", "data": { "order_id": "999", "name": "Alice" } }
  • Response: 202 Accepted
{ "notification_id": "notif_abc123", "channels": ["push", "email"] }
  • Error: 429 Too Many Requests (quota exceeded).

Four fields decide how the rest of the design behaves.

channels is a list, and it is optional. Step 2 asks for delivery on one or several channels, so a single channel field could not express the requirement. Sending the list explicitly is the common case. Leaving it out means "use whatever this user has enabled", which is what a caller wants when it does not know or care how the person prefers to be reached. Either way the Preference Service in Step 5 makes the final decision: a channel the user has switched off is dropped from the list before anything is queued.

priority names urgency, not a channel. Step 2 says channels are never ranked against each other, and that still holds: email is not served ahead of SMS. Urgency is a separate question, and it is one the system has to answer. The accepted values are transactional, default and bulk. Step 7 gives each of them its own queue per channel, so a one-time passcode is never stuck behind a ten million message campaign.

idempotency_key is generated by the caller, one value per real-world event, and reused on every retry of that event. Delivery here is at least once, so a caller that retries after a timeout would otherwise send the same message twice. The service stores the key with its result for 24 hours and returns the original notification_id for a repeat.

202 Accepted, not 200 OK. The response means the request is queued, not that anybody has been notified. Delivery happens later and can still fail. Returning 200 would imply a promise this endpoint cannot make.

2. Get Status

  • GET /v1/notifications/{notification_id}
  • Response:
{ "status": "partial", "channels": { "push": { "status": "delivered", "updated_at": "..." }, "email": { "status": "failed", "reason": "hard_bounce" } } }

Status is reported per channel, because one notification can succeed on push and fail on email. A single top-level status would have to pick one of them and hide the other, and the failed one is usually the one the caller needs.

3. Update Preferences

  • PUT /v1/users/{user_id}/preferences
  • Body: { "categories": { "marketing": false, "shipping": true } }

Opting out is a PUT on the user's own preferences rather than a flag on a notification, so it applies to every future send from every caller. A tenant cannot override it by setting a field on a request.

Next: Step 5, where the services behind that API are laid out.

On This Page

Step 4: API Specifications