hub.gazar.dev · senior-to-staff · worked system design

Design WhatsApp: chat & messaging

A fully worked answer to the second most common prompt, in the course order: requirements, numbers, the realtime protocol, data, the high-level diagram, the deep dives that actually decide it, then reliability, observability, and the trade-offs. The lever here is the persistent connection: how do you hold hundreds of millions of live sockets and route a message between any two of them, in order, even when one side is offline. Part two then reuses that exact engine and rings it with the WhatsApp Business services: templates, the 24-hour window, conversation billing, quality tiers, reliable webhooks, catalog, payments, Flows, and the agent inbox. Back to the course.

The prompt, scoped

"Design WhatsApp" is really "design realtime messaging at scale." We keep the spine: 1:1 and small-group messages, delivered in order, with sent / delivered / read receipts and presence, working even when the recipient is offline. We name the rest as out of scope in the first minute: voice and video calls, media transcoding (a blob-store-plus-CDN aside), and the internals of end-to-end encryption (we note where it sits, we do not design the crypto).

Step 1

Requirements: functional and non-functional

The functional core:

Out of scope (say it): calls, media pipelines, E2E-encryption internals, status/stories.

The non-functional shape is unusual, and it is what makes this hard:

realtime · low latency hundreds of M live connections in-order per conversation durable until delivered availability over consistency

The defining fact is not request volume, it is connection volume: a messaging system is mostly a fleet that holds an enormous number of long-lived sockets and shuttles small payloads between them. Hold that.

Maps to S2 (requirements to numbers) and S1 (consistency vs availability).

Step 2

Estimation: the numbers that bound the design

QuantityAssumptionResult
Users2B users, ~500M concurrently onlinethe population
Messages~100B messages/day~1.2M msgs/sec avg, multiples at peak
Concurrent connections~500M live socketsthe hard number
Sockets per gateway~1M per box (tuned kernel)~500 connection-gateway boxes just to hold sockets
Message size~1 KB typicalpayloads are tiny; the cost is connections, not bandwidth
Storagestore only until delivered (1:1)server is store-and-forward, not an archive

The number that decides the architecture

~500M concurrent persistent connections. That single figure forces a dedicated tier whose only job is to hold sockets, a registry to find which box holds whom, and a design where the message itself is small and cheap. Everything else follows from "where is the recipient's socket right now?"

Maps to S2 (capacity estimation).

Step 3

Protocol: why this is not REST

The hot path is realtime push, so the client holds a persistent connection (a WebSocket, or an XMPP-style long connection) to a gateway, rather than polling a REST endpoint. The few operations on that connection:

OpDirectionNotes
SEND {convId, clientMsgId, body}client → serverclientMsgId dedupes a retried send (idempotent)
MESSAGE {msgId, seq, body}server → clientpushed down the socket; carries the per-conversation seq
RECEIPT {msgId, state}bothdelivered / read acknowledgements
PRESENCE {userId, state}bothonline / last-seen, debounced

REST is fine for the cold edges (auth, contact sync, media upload URLs). The realtime spine is the long-lived socket.

Those four ops in motion, for a 1:1 message with both users online. Notice the shape the whole system inherits: the server acks the sender the moment it has the message durably, then the delivered and read receipts flow back asynchronously as Bob's device reacts. Nothing blocks on the recipient.

sequenceDiagram
    autonumber
    actor A as Alice (online)
    participant GW as Conn gateway
    participant MS as Message service
    actor B as Bob (online)
    A->>GW: SEND {convId, clientMsgId, body}
    GW->>MS: forward
    MS->>MS: persist, assign msgId + per-conv seq
    MS-->>A: ACK (server has it durably)
    MS->>B: MESSAGE {msgId, seq, body}
    B-->>MS: RECEIPT {msgId, delivered}
    MS-->>A: RECEIPT {msgId, delivered}
    Note over B: Bob opens the chat
    B-->>MS: RECEIPT {msgId, read}
    MS-->>A: RECEIPT {msgId, read}
The realtime protocol as a sequence: one idempotent SEND up, one MESSAGE pushed down, and two RECEIPT hops back (delivered, then read). The sender is acked before delivery, so send latency never depends on whether Bob is looking at his phone.

Maps to S5 (API and protocol choice): persistent connection, idempotent send. See the API cheat sheet.

Step 4

Data model and store choices

User(id PK, ...)
Device(id PK, user_id)                         a user has many devices
Conversation(id PK, type=1to1|group, members)
Message(id PK = Snowflake, conv_id, sender_id, seq, body, sent_at)
Mailbox[user_id] -> queue of undelivered message ids    store-and-forward
SessionRegistry[user_id] -> gateway_id (+ device)       who holds the live socket
Presence[user_id] -> {state, last_seen}

stores
  Messages / mailbox .. write-heavy, time-ordered, per-user queues   -> Cassandra / wide-column
  Session registry .... fast lookup user -> gateway, ephemeral        -> Redis
  Presence ............ ephemeral, TTL'd, high churn                  -> Redis

Why these stores

  • Messages and mailboxes are append-heavy and read by recipient in order: a wide-column store (Cassandra-style) partitioned by user, with a per-conversation seq for ordering. The server deletes once delivered (1:1), so it is a buffer, not an archive.
  • Session registry is the lookup the whole system leans on: given a recipient, which gateway holds their socket? Ephemeral and hot, so Redis.
  • Presence is high-churn and disposable, TTL'd in Redis; stale is acceptable.

Maps to S3 (store from access patterns) and S4 (partition the mailbox by user). See the data-stores cheat sheet.

Step 5

High-level architecture

Clients hold a persistent socket to a connection gateway. To deliver a message, the message service looks up the recipient in the session registry (which gateway holds their socket) and routes it there to be pushed down. If the recipient is offline, it lands in their mailbox and is delivered on reconnect. Presence is a separate, lossy service. The whole point is decoupling "hold the socket" from "route the message."

Alice app · socket Bob app · socket Conn gateway A holds Alice socket Conn gateway B holds Bob socket Message service route · persist · receipts Session registry user → gateway · Redis Mailbox store offline queue per user Presence Redis · TTL WebSocket WebSocket SEND deliver where is Bob? if offline: store presence
Green = persistent sockets and the lookups that route a message. Alice sends to gateway A; the message service asks the session registry which gateway holds Bob, then routes to gateway B to push down his socket. If Bob is offline it goes to his mailbox and is delivered on reconnect. Presence is a separate, lossy Redis service.

Inside the message service: the modules

At the high level the message service is one box, because at an interview you first defend the boundaries: gateways hold sockets, one service routes and persists, presence is off to the side. But that box is not a monolith you would build as a single blob. It is one deployable made of a handful of modules that share the hot path, so a message passes through them in a fixed order rather than crossing a network between each. Zooming into that box:

ModuleIts one jobStore it touchesIf it fails
Ingest & idempotencyaccept SEND off the gateway; dedupe a retried send by clientMsgId so a retry never doubles a messagerecent-id cachereject and let the client retry; never a duplicate
Sequencer & persistenceassign the Snowflake msgId and the per-conversation monotonic seq, then write the message durablyMessage store (Cassandra)fail closed: no SENT ack until the write lands
Router / dispatcherread SessionRegistry[recipient]; online forward to that gateway, offline or miss hand to the mailboxSession registry (Redis)a miss is treated as offline, so it costs latency, never a message
Mailbox managerstore-and-forward: append for offline recipients, drain in seq order on reconnect, delete once deliveredMailbox storethe durability backbone: acked only once durably queued
Receipt managerdrive the SENTDELIVEREDREAD state machine forward-only, dedupe replays by msgId, forward receipts to the senderMessage storereplayed receipts are no-ops; ticks never move backward
Fan-outexpand a group SEND into N per-member deliveries, bounded by the group-size cap, each reusing router + mailboxbounded by the cap, so no celebrity / hot-key blow-up

The modules chained on the path a single message actually walks: in through ingest, down through the sequencer that makes it durable, then the router's one lookup with two outcomes, with the receipt manager riding the acks back to the sender and fan-out sitting in front of the router for the group case.

flowchart TD
    IN["SEND from gateway"] --> ING["Ingest and idempotency
dedupe by clientMsgId"] ING --> SEQ["Sequencer and persistence
assign msgId plus per-conv seq, durable write"] SEQ --> ACK["ACK SENT to sender"] SEQ --> RT{"Router / dispatcher
SessionRegistry lookup"} FO["Fan-out
group send expands to N, bounded by cap"] --> RT RT -->|"online"| PUSH["Forward to recipient gateway, push down the live socket"] RT -->|"offline or miss"| MB["Mailbox manager
append durable, drain in seq order on reconnect"] PUSH --> RCPT["Receipt manager
forward-only state machine, dedupe by msgId"] MB --> RCPT RCPT --> BACK["Forward DELIVERED then READ to the original sender"] classDef good fill:#D6F5E3,stroke:#1B7F46,color:#09244B classDef accent fill:#DCEBFE,stroke:#4A90D9,color:#09244B class SEQ,MB good class RT accent
The same box, opened up. One message threads ingest → sequence-and-persist → route, branching online (push) or offline (mailbox), with the receipt manager carrying the acks back. Green marks the two durability guarantees (persist before ack, mailbox before offline), blue the single routing decision the whole system hinges on. Fan-out is the only extra hop, and only for groups.

Why these are modules, not services

Ingest, sequencing, routing, mailbox, and receipts all sit on the same message's hot path and share its transaction, so splitting them into separate services would only buy you a network hop and a distributed transaction per message. They ship as one deployable. The two things that genuinely are separate services earn it on a different axis: the connection gateway is memory-bound (it does nothing but hold ~1M sockets) and presence is lossy and must be bulkheaded so a presence storm can never touch delivery. Boundaries follow scaling axis and blast radius, not the org chart.

Maps to S7 (architectural styles, service vs module boundaries) and S6 (routing, queues, idempotent delivery).

Step 6

Deep dive A: holding hundreds of millions of connections

The signature challenge. You cannot route a message to a user until you know which box currently holds their live socket, and that mapping changes constantly as people connect, drop, and roam.

connection gateway
  - one job: terminate ~1M long-lived sockets, nothing else
  - on connect:   write SessionRegistry[user] = this_gateway
  - on disconnect: clear it (and heartbeat to detect dead sockets)
  - stateless beyond the sockets it holds, so it scales horizontally

routing a message to Bob
  1. message service reads SessionRegistry[Bob] -> gateway B (or "offline")
  2. online?  forward to gateway B, which pushes down Bob's socket
  3. offline? append to Mailbox[Bob]; deliver when he reconnects

The registry is populated by the connection lifecycle itself. Every connect writes the mapping, every disconnect (or a missed heartbeat) clears it, so the registry is always an eventually-consistent picture of who is reachable where.

stateDiagram-v2
    [*] --> Connecting
    Connecting --> Online: socket established
    Online --> Online: heartbeat OK
    Online --> Offline: disconnect / missed heartbeat
    Offline --> Connecting: client retries
    Offline --> [*]
    note right of Online
        SessionRegistry[user] = this gateway
        messages route here, pushed live
    end note
    note left of Offline
        registry entry cleared
        new messages fall to the mailbox
    end note
A socket's lifecycle drives the registry. Online means "the registry points at my gateway, push to me"; Offline means "the entry is gone, buffer me." Heartbeats exist so a dead socket becomes Offline quickly rather than silently black-holing messages.

And the routing decision every message runs through, one lookup with two outcomes:

flowchart TD
    M["Message for Bob"] --> L{"SessionRegistry[Bob]?"}
    L -->|"online → gateway B"| R["Route to gateway B"]
    R --> P["Push down Bob's live socket"]
    P --> RC["RECEIPT: delivered"]
    L -->|"offline / miss"| MB["Append to Mailbox[Bob] (durable)"]
    MB --> W["Bob reconnects"]
    W --> D["Gateway drains mailbox in seq order"]
    D --> RC
One question, two branches. The online branch is a memory hop through a gateway; the offline branch is a durable write to the mailbox. A registry miss is treated exactly like offline, so a stale or lost lookup costs latency, never a message.

Why split "hold the socket" from "route the message"

Connection gateways are memory-bound (sockets), the message service is CPU/IO-bound (routing, persistence). Separating them lets each scale on its own axis, and lets a gateway die without losing messages: the client reconnects to another gateway, the registry updates, and the mailbox covers anything missed in between.

Maps to S4 (sharding the connection tier) and S7 (stateless gateways behind a registry).

Step 6 · continued

Deep dive B: delivery semantics and ordering

The receipts every user sees are a state machine, and getting them right under retries is the real work. The network will deliver twice; the design makes that harmless and keeps order within a conversation.

delivery state machine (per message)
   SENT ──(recipient's device acks)──► DELIVERED ──(opened)──► READ

ordering
  - each conversation carries a monotonic seq; clients render by seq,
    not by arrival time (sockets can reorder)
ordering scope
  - per-conversation, NOT global. global ordering is unnecessary and
    expensive; only "messages within this chat" must be in order

exactly-once, honestly
  - delivery is at-least-once; clientMsgId + msgId dedupe on both ends
  - => a retried SEND or a re-pushed MESSAGE applies once

The receipt ticks users see are exactly this state machine, and it only ever moves forward. Idempotent ids are what let a duplicate MESSAGE or a replayed RECEIPT re-assert a state without ever moving it backward.

stateDiagram-v2
    [*] --> Sent: server persisted (one tick)
    Sent --> Delivered: recipient device acks (two ticks)
    Delivered --> Read: recipient opens chat (two blue ticks)
    Read --> [*]
    Sent --> Sent: duplicate SEND (dedupe by clientMsgId)
    Delivered --> Delivered: re-pushed MESSAGE (dedupe by msgId)
A strictly forward state machine per message: SENT → DELIVERED → READ. At-least-once delivery means each transition can fire more than once; deduping on clientMsgId and msgId makes the repeats no-ops, so the ticks never flicker backward.

The senior point

Two moves score here. First, scope ordering to the conversation, not the globe, so you never pay for global consensus. Second, accept at-least-once delivery and make it idempotent with message ids, rather than chasing a true exactly-once that does not exist over a network.

Maps to S6 (idempotency, at-least-once) and S1 (consistency scope). See the resilience cheat sheet.

Step 6 · continued

Deep dive C: offline delivery and groups

Most messages are sent to someone not currently looking at their phone, so store-and-forward is the norm, not the edge case. Groups are fan-out on top of it.

offline (store-and-forward)
  recipient offline -> append to Mailbox[recipient]
  on reconnect      -> gateway drains the mailbox in seq order, then
                       deletes delivered entries

groups (small)
  send to group of N -> fan out to N mailboxes / sockets
  the WhatsApp choice: cap group size; do the fan-out server-side
  (a "fan-out on write" exactly like the timeline, but bounded by
   the group-size cap, so no celebrity problem)

multi-device
  a user has several devices -> deliver to each device's session,
  track per-device delivery so receipts are correct

Store-and-forward as a sequence: the mailbox absorbs the message while Bob is dark, and his reconnect is what triggers an ordered drain. This is the common case, not the exception.

sequenceDiagram
    autonumber
    participant MS as Message service
    participant MB as Mailbox[Bob]
    participant GW as Conn gateway
    actor B as Bob
    Note over B: Bob is offline (no registry entry)
    MS->>MB: append message (durable)
    MS-->>MS: ack sender as SENT
    Note over B: later — Bob reconnects
    B->>GW: connect (new socket)
    GW->>MB: drain undelivered in seq order
    MB-->>GW: queued messages
    GW->>B: MESSAGE ... (ordered by seq)
    B-->>GW: RECEIPT delivered
    GW->>MB: delete delivered entries
The mailbox is a per-user buffer, not an archive: messages live there only until the recipient reconnects, drains them in seq order, and acks. Once delivered, the entries are deleted, which is why 1:1 storage stays bounded.

Groups are just this, fanned out at write time, deliberately bounded by a group-size cap so the fan-out can never explode into the celebrity / hot-key problem that a public timeline has.

flowchart LR
    S["Alice SENDs to group G
(N members, N ≤ cap)"] --> F["Fan-out on write
server-side"] F --> M1["member 1
socket or mailbox"] F --> M2["member 2
socket or mailbox"] F --> M3["member N
socket or mailbox"]
Bounded fan-out on write: each group send expands into N per-member deliveries, each resolving to a live socket (online) or a mailbox append (offline). Capping N keeps the fan-out cost fixed, so groups reuse the entire 1:1 machinery unchanged.

Maps to S4 (fan-out, bounded) and S9 (the mailbox is the durability guarantee).

Step 7

Reliability & failure design

If this fails…ModeWhat the user sees
A connection gateway diesdegradeclient reconnects to another gateway; registry updates; mailbox covers the gap, no message lost
Presence servicefail openstale or missing "last seen"; messaging unaffected
Message persistencefail closeddo not ack SENT until durably stored; the client retries
Session registry missdegradetreat recipient as offline, store in mailbox; deliver on next connect

The mailbox is the durability backbone: a message is acknowledged only once it is durably queued, so a gateway crash or a missed lookup costs latency, never a lost message. Presence is deliberately the lossy, fail-open service, bulkheaded from the messaging path so a presence storm cannot touch delivery. Cross-service calls are bounded with timeouts, idempotent retries, and breakers.

Maps to S9 (designing for failure): fail closed on the message, open on presence.

Step 7 · continued

Observability & operability

Maps to S10 (observability). See the observability cheat sheet.

Step 8

Trade-offs along the six core axes

Axis (S1)The call hereWhy it is least-wrong
Consistency vs availabilityavailability + per-conversation orderglobal order is needless; the chat must stay deliverable
Latency vs costpersistent socketspay memory to hold connections, buy realtime push over polling
Push vs pullpush down the socketmessaging is inherently server-initiated; pull would be wasteful and slow
Stateful vs statelessthin stateful gateways + registryisolate the one stateful thing (the socket) so the rest scales statelessly
Durability vs throughputack only after the mailbox writenever lose a message; accept a little send latency for it
Simplicity vs capability1:1 first, groups as bounded fan-outcap group size so fan-out never becomes the celebrity problem

The whole design is one question answered well: where is the recipient's socket right now? Hold sockets in a dedicated tier, find them through a registry, fall back to a durable mailbox, and keep order per conversation. Say that and the boxes follow.

Maps to S1 (the six core trade-offs) and S12 (present and defend).

Part two

Extending to WhatsApp Business: every service around the core

Everything above is consumer messaging: two people, symmetric, peer-to-peer. WhatsApp Business turns one side of the conversation into a company, and that single change ripples outward. Messages are now often programmatic (an API call, not a thumb on a phone), frequently business-initiated and therefore templated and consented, metered and billed, gated by a quality score, and wired to a catalog, payments, and a support-agent inbox. The good news for the design: the delivery engine from Part one (gateways, message service, mailbox, receipts, session registry) is reused unchanged. Business is a ring of services layered around it, each earning its own boundary.

programmatic API, not a phone business-initiated + consented templated, policy-reviewed metered + billed per conversation quality-gated rate tiers reliable outbound webhooks

Step 9

The surface: everything WhatsApp Business does

Name the whole surface before you draw a box, grouped by what it is for:

AreaCapabilities
Two front doorsthe Business App (small business, one phone, human-typed, shared-agent inbox, labels, quick replies, away messages) and the Business Platform / Cloud API (programmatic, used by mid-market and solution providers, one phone-number-id, access-token auth)
Message typessession / free-form inside the 24-hour customer-service window; templates (HSM) for business-initiated sends, in categories marketing, utility, authentication (OTP); interactive (reply buttons, list menus, call-to-action URLs); Flows (multi-screen forms); media and documents
Commerceproduct catalog, single and multi-product messages, carts, orders, and WhatsApp Pay where available
Automation & entrychatbots and automated flows, click-to-WhatsApp ads, click-to-chat links, QR codes
Trust & identityverified business profile, display-name review, the green-tick official-business badge
Operationswebhooks for inbound messages and delivery statuses, conversation-based pricing, quality rating and per-number messaging limits, template review/approval, opt-in / consent ledger, analytics

The one idea that organizes all of it

A business cannot simply spam a user. Consumer messaging is consented by mutual contact; business messaging is permissioned. So almost every Business service exists to answer one of three questions: are you allowed to send this? (consent, templates, quality), what does it cost? (conversation metering), and how does the business system hear back? (webhooks). Hold those three and the service list writes itself.

Maps to S2 (scope the functional surface) and S1 (permissioned vs open).

Step 10

All the services: the extended architecture

The core delivery engine sits in the middle, untouched. Around it, each Business concern is its own service, drawn by the boundary rule from Part one: separate on scaling axis or blast radius, not on the org chart.

ServiceIts one jobWhy a separate service
Business API gatewayterminate the Cloud API (Graph-style REST); authenticate by access token + phone-number-id; validate and hand well-formed sends to the corepublic internet edge with its own auth, versioning, and request rate limits
Template mgmt & approvalsubmit, policy-review (automated + human), version, and store approved templates; fill variables at send timereview is slow and human-in-the-loop; it must not sit on the send hot path
Quality & rate-limithold each number's tier (250 → 1K → 10K → 100K → unlimited per day) and quality rating; throttle or block sendsfed by async signals (blocks, reports); protects users and the platform
Conversation & billingopen and close 24-hour conversation windows by category, count billable conversations, emit usage recordsmoney path: must be auditable and exactly-counted, decoupled from delivery
Webhook deliveryreliably push inbound messages and status callbacks to each business's HTTPS endpoint, signed, retried, per-business orderedoutbound to untrusted, flaky endpoints; needs its own queue and retry brain
Catalog & commercestore products; render product, cart, and order messages; track order staterelational, read-heavy, its own domain model
PaymentsWhatsApp Pay: payment-provider integration, tokenized instruments, settlementPCI scope and regulatory isolation, region-specific
Flowsserve multi-screen interactive forms; exchange encrypted data with the business endpointa stateful form runtime with its own encryption boundary
Profile & verificationprofile fields, display-name approval, green-tick badge statean identity and trust workflow with human review
Agent inbox & routingassign conversations to human agents, labels, agent presence (Business App and partner inboxes)a CRM-shaped app, not the delivery hot path
Consent / opt-inthe ledger of who opted in to what; block and report intake that feeds qualitythe compliance record of truth, audited

The same picture, wired: business systems call the API gateway, which checks template, quality, and metering before handing a send to the core engine; the core delivers exactly as in Part one; inbound messages and statuses fan back out through the webhook service to each business's own servers.

flowchart TD
    subgraph BIZ["Business systems, their own servers"]
      BAPP["Business app or CRM"]
      WH["Their webhook endpoint"]
    end
    subgraph CLOUD["WhatsApp Business Platform, the new ring"]
      GWAPI["Business API gateway
Cloud API, token auth, phone-number-id"] TMPL["Template mgmt and approval
submit, review, versioned store"] QUAL["Quality and rate-limit
per-number tier, quality rating"] METER["Conversation and billing
24h windows by category, usage"] WHD["Webhook delivery
reliable outbound, retried, signed"] CAT["Catalog and commerce
products, carts, orders"] PAY["Payments
WhatsApp Pay, provider settle"] FLOW["Flows
multi-screen forms, encrypted"] PROF["Profile and verification
display name, green tick"] INBOX["Agent inbox and routing"] CONS["Consent and opt-in ledger"] end subgraph CORE["Core delivery engine, reused unchanged"] MS["Message service"] GW["Conn gateways"] MB["Mailbox"] SR["Session registry"] end U["Consumer WhatsApp app"] BAPP -->|"send template or reply"| GWAPI GWAPI --> TMPL GWAPI --> QUAL GWAPI --> METER GWAPI --> CONS GWAPI --> MS MS --> GW GW --> U U -->|"inbound or status"| MS MS --> WHD WHD --> WH GWAPI --> CAT CAT --> PAY GWAPI --> FLOW GWAPI --> PROF GWAPI --> INBOX style CORE fill:#D6F5E3,stroke:#1B7F46,color:#09244B style CLOUD fill:#DCEBFE,stroke:#4A90D9,color:#09244B style BIZ fill:#FFEDD5,stroke:#C2410C,color:#09244B
Green is the Part-one engine, reused untouched. Blue is the new Business ring: the API gateway is the single front door, and it must clear a send past template, quality, and metering before the core will deliver it. Orange is the business's own systems. Note the two directions: sends flow left-to-right into the core; inbound messages and statuses flow back out through the webhook service to the business.

Maps to S7 (service decomposition) and S6 (each concern as a bounded service).

Step 10 · continued

Deep dive D: templates and the 24-hour window

This is the concept that defines business messaging and the one interviewers probe. A business may not message a user freely. There are two modes, and which one applies is a function of time since the user last spoke:

business-initiated (no open window)
  must use a PRE-APPROVED template (HSM)
  category = marketing | utility | authentication
  variables filled at send; body and layout are fixed and reviewed

user-initiated (or a business reply within 24h)
  a 24h "customer-service window" is OPEN
  free-form + interactive messages allowed, no template needed

the rule
  the window OPENS when the USER messages the business
  it lasts 24h from the last user message
  outside the window -> template only

The window as a state machine. It is the mechanism that makes business messaging permissioned: a company can always answer a user who reached out, but to start a conversation it must fall back to a reviewed template, and the category of that template is what it gets billed on.

stateDiagram-v2
    [*] --> Closed
    Closed --> ServiceWindow: user messages the business first
    ServiceWindow --> ServiceWindow: within 24h, free-form allowed
    ServiceWindow --> Closed: 24h elapses with no new user message
    Closed --> TemplateOnly: business wants to re-engage
    TemplateOnly --> ServiceWindow: user replies, a new window opens
    note right of ServiceWindow
        billable conversation open
        free-form and interactive allowed
    end note
    note right of TemplateOnly
        only pre-approved templates
        marketing, utility, or authentication
    end note
Two modes gated by a 24-hour timer. Inside the service window the business talks freely; outside it, only a pre-approved template can reopen the door, and only if the user answers. Templates are reviewed offline so the send path stays fast: at send time the template is already approved and you are just filling variables.

The senior point

Two design moves fall out of the window. First, template review is asynchronous, decoupled from the send path: you approve slowly (ML plus human), then sends are a fast variable-fill against an already-approved artifact. Second, the window is a per-conversation timer, exactly the kind of state you keep in a fast, TTL'd store (the same Redis instinct as presence), not a distributed transaction.

Maps to S1 (permission as a consistency rule) and S6 (async approval, TTL'd window state).

Step 10 · continued

Deep dive E: reliable outbound to business servers

In Part one the mailbox buffered messages for an offline phone. Webhook delivery is the same pattern turned inside out: it buffers events for a flaky business endpoint. Every inbound message and every status change has to reach the business's own server, and that server will be down, slow, or return errors.

what must reach the business
  - inbound: a user messaged them
  - statuses: sent / delivered / read / failed for their outbound sends

guarantees (same spine as the mailbox)
  - at-least-once + idempotency: each event carries a stable id;
    the business dedupes, so a retry is harmless
  - per-business ordered queue, so statuses arrive roughly in order
  - HMAC-signed payloads, so the business can trust the source
  - bounded retries with backoff; a dead-letter after N failures
flowchart TD
    EV["Event: inbound message or status update"] --> Q["Per-business ordered queue"]
    Q --> D["Webhook dispatcher"]
    D --> POST["HTTPS POST to business callback URL, HMAC signed"]
    POST -->|"2xx"| OK["Ack and drop from queue"]
    POST -->|"error or timeout"| RETRY["Backoff, retry at-least-once"]
    RETRY --> D
    OK --> IDEM["Business dedupes by event id"]
    classDef good fill:#D6F5E3,stroke:#1B7F46,color:#09244B
    classDef bad fill:#FEE4E2,stroke:#C8102E,color:#09244B
    class OK good
    class RETRY bad
Outbound webhooks are store-and-forward again: a durable per-business queue, a dispatcher that POSTs signed payloads, and at-least-once retries with backoff. The business makes it exactly-once on its side by deduping on the event id, the mirror of how the consumer client dedupes a re-pushed MESSAGE.

Why this reuses Part one thinking

Nothing here is new physics. A durable queue, at-least-once delivery, idempotent ids, ordering scoped to one partition (here, one business), and fail-closed durability are the same tools that carried the consumer mailbox. Saying that out loud (the webhook path is a mailbox for businesses) is the move that shows you designed a system, not a pile of features.

Maps to S9 (designing for failure) and S6 (idempotency, at-least-once, ordered partitions).

Step 10 · continued

Deep dive F: quality, rate tiers, and conversation billing

Two feedback loops guard the platform. Quality keeps businesses from spamming; billing is how the whole thing pays for itself. Both hang off the same conversation object.

Messaging limits escalate with proven good behaviour and collapse when users push back:

TierBusiness-initiated conversations / dayMove up whenMove down when
New / unverified~250 unique usersverify the business, keep quality high
Tier 11Ksustained volume at good qualityquality rating drops to low (blocks, reports)
Tier 210K
Tier 3100K
Tier 4unlimitedtop tier, quality holdsdemotion on quality collapse

Quality rating (high / medium / low) is computed from user signals: blocks, reports, and how many messages go unread. It is an async, lossy score, so it lives beside the number's state and throttles sends; it never sits synchronously on the hot path.

Conversation-based billing meters the 24-hour windows, priced by category, not per message:

CategoryWho opens itRelative cost
Serviceuser-initiated (the customer-service window)cheapest / often free-tier
Utilitybusiness, transactional (receipts, order updates)low
Authenticationbusiness, one-time passcodeslow
Marketingbusiness, promotionalhighest

The senior point

Billing is the money path, so it inherits the message service's discipline turned up a notch: a conversation is counted exactly once per 24-hour window using an idempotency key on (business, user, category, window), and usage is emitted as durable, auditable events. You meter off the same conversation-window object that gates permission, so one piece of state does double duty: it decides what may be sent and what it costs.

Maps to S2 (the numbers that bound cost) and S9 (exactly-counted money path, async quality signals).

Step 11

What you reused, what you added

The interview-winning summary is to show the seam clearly: the hard realtime engine did not change; Business is a permission-and-commerce ring bolted around it.

Reused from Part one (unchanged)Added for Business (the ring)
Connection gateways holding socketsBusiness API gateway (programmatic front door)
Message service: route, persist, receiptsTemplate approval, quality / rate-limit, metering
Mailbox store-and-forward for offline usersWebhook delivery: store-and-forward for offline businesses
Session registry, presenceConversation-window state (TTL'd, like presence)
At-least-once + idempotent ids, per-partition orderConsent ledger, catalog, payments, Flows, agent inbox

The one sentence for Part two: WhatsApp Business is the same delivery engine wrapped in a permission layer (consent, templates, quality), a metering layer (conversation billing), and a callback layer (reliable webhooks), plus commerce services (catalog, payments, Flows) hung off a programmatic API gateway. Everything hard about it, ordering, durability, idempotency, you already solved in Part one and reused by name.

Maps to S12 (present and defend: reused engine, new ring) and S7 (boundaries by axis and blast radius).

Use this in the cohort