</> MAANG.io
system design · 301

Advanced

Master system design interviews with scalable architecture patterns, distributed systems, and real-world design challenges.

0/480 solved 0% complete

Section 4: Broadcast, Don't Command - API Design (with Pushback)

Executive Summary: The Privacy Request Orchestrator has two audiences who should never see each other's API. Users get four endpoints that treat a privacy request as a long-running operation: submit it, watch its phase, cancel it during the grace period, download the export. The 1,200 registered data holders get a different contract: a registration manifest, a broadcast event that carries no personal data, a scoped identifier lookup, and an idempotent PUT acknowledgement bound to their mTLS identity. This section walks both faces with Lena's requests and defends five choices under pushback: why there is no DELETE webhook, why 202 and not 200, why users can't see per-service progress, why acks are PUT, and why the event leaves out her email.

Next prompt: "Show me the API." The tempting answer is one line, DELETE /users/{id} on every service. You draw a line down the middle of the board instead.

"There are two APIs here, and they have opposite jobs. The left side is a promise to a person: we've got your request, here's your deadline, here's how to change your mind. The right side is a contract with 1,200 services: here's an obligation, tell us what you did, and prove it was you. The orchestrator never deletes anything itself; it hands out obligations and records the answers."

The User Face: A Long-Running Operation

Lena has re-authenticated with 2FA. A logged-in session alone isn't enough here: a stolen session must never become an irreversible deletion. She clicks "Delete my account":

POST /v1/privacy-requests
Cookie: sid=...                       (session re-authenticated with 2FA moments ago)
Idempotency-Key: 6f1e2a9c-4b7d-4e0a-9d3f-1c2b7a8e5d40
Content-Type: application/json

{ "type": "ERASURE", "scope": "ALL" }

202 Accepted
Location: /v1/privacy-requests/dsr_01HQZ…DEL
{
  "request_id": "dsr_01HQZ…DEL",
  "state": "RECEIVED",
  "legal_deadline": "2026-04-09T09:12:00Z",
  "cancel_until":   "2026-03-24T09:12:00Z"
}

Both timestamps are computed once at receipt and stored. legal_deadline is the earlier of receipt plus 30 days and the same date next month (EU time-limit counting, which bites for late-January requests); cancel_until is receipt plus the 14-day grace period. Returning them in the first response makes the deadline part of the contract. The Idempotency-Key covers the double-click and the mobile retry: the same key returns the same request_id, so two clicks never create two grace windows. As a second guard, the server allows one open erasure per subject.

Polling returns the phase only:

GET /v1/privacy-requests/dsr_01HQZ…DEL

200 OK
{ "request_id": "dsr_01HQZ…DEL", "type": "ERASURE", "state": "GRACE",
  "legal_deadline": "2026-04-09T09:12:00Z", "cancel_until": "2026-03-24T09:12:00Z" }

Users see five phases: RECEIVED, GRACE, IN_PROGRESS, COMPLETED, CANCELLED. The finer internal states in Section 7 (fanning out, awaiting acks, verifying) all show up as IN_PROGRESS. If the deadline is extended, the response also carries extended_deadline and the reason, because the user must be told. The way back is POST /v1/privacy-requests/{id}:cancel, which works only in GRACE. After day 14 it returns 409 Conflict: fan-out has begun and deletion can no longer be undone.

Her export from earlier in the month uses the same resource with { "type": "ACCESS", "scope": ["mail", "maps", "photos"] }. When it's ready, she sees the chunk list, and each download needs a fresh re-authentication:

GET /v1/privacy-requests/dsr_01HQX…EXP/export

200 OK
{ "total_bytes": 25769803776, "available_until": "2026-03-12T10:40:00Z",
  "chunks": [ { "part": 1, "bytes": 2147483648 }, …12 chunks of 2 GB, 24 GB in total… ] }

POST /v1/privacy-requests/dsr_01HQX…EXP/export/chunks/1:download   (fresh re-auth)

200 OK
{ "url": "https://export.example.com/…?sig=…", "expires_at": "2026-03-05T10:55:00Z" }

The archive stays staged for 7 days, but a signed URL is a bearer token, so each one covers one chunk and lives 15 minutes. A forwarded link dies quickly.

Sequence: Lena's browser POSTs an erasure request with an Idempotency-Key, the Intake API checks re-auth and 2FA, commits the request to the Request Ledger and returns 202 RECEIVED; days later a GET shows state GRACE with cancel_until

Intake averages about one request per second, sized for 30/s bursts, p99 under 500 ms. The 202 is sent only after the multi-region commit, so an accepted request can't be lost (RPO 0) and a failed one fails loudly for a retry.

Quick check before moving on:

Sign in to continue reading

The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.

Sign in to read the full lesson

Sign in to MAANG.io

Use your Google or Microsoft account — no password to remember.

Continue with Google Continue with Microsoft

Please accept the terms above to continue.