</> 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: Messages, Not Endpoints - API Design (with pushback)

Executive Summary: This section designs the contract for both features as messages on the meeting's existing WebSocket, not as REST endpoints. Reactions get one fire-and-forget message up and one coalesced frame down, with no acknowledgement and a one-second shelf life. Raise hand gets commands with client-generated opIds, server acks and rejects, versioned deltas, and snapshots. We then defend six choices under pushback: no REST, no acks for reactions, counts instead of clicks, server-assigned order, opId on an "idempotent" command, and the epoch field.

The interviewer caps the marker: "Walk me through your API." You start with the observation that shapes everything else.

You: "Every participant already has a persistent signaling connection open for the roster and mute state. Reactions and hands ride that socket as typed messages. The only REST endpoint is for meeting options, which change a few times per meeting at most."

The Message Catalog

Every message is a small JSON object whose msg field names it; the rest is its payload. JSON is fine for v1; a binary encoding later changes bytes, not semantics.

Client to server:

{ "msg": "reaction.send", "meetingId": "mtg-7f3a", "type": "applause", "count": 2, "clientTs": 1793293923250 }
{ "msg": "hand.raise",    "meetingId": "mtg-7f3a", "opId": "9c1e…" }
{ "msg": "hand.lower",    "meetingId": "mtg-7f3a", "opId": "c41d…", "targetParticipantId": "p-sam" }
{ "msg": "hand.lowerAll", "meetingId": "mtg-7f3a", "opId": "e07a…" }
{ "msg": "meeting.sync",  "meetingId": "mtg-7f3a", "haveVer": 3 }

targetParticipantId is optional: absent means "lower my own hand." haveVer lets the owner answer "you're current" when it matches; otherwise the reply is a full hand.snapshot.

Server to client:

{ "msg": "reaction.burst", "meetingId": "mtg-7f3a", "tick": 5120,
  "counts": { "like": 0, "heart": 3, "applause": 212, "laugh": 0, "surprised": 1 },
  "senders": ["p-maya", "p-0193", "…up to 10"] }
{ "msg": "hand.ack",    "opId": "9c1e…", "ver": 4 }
{ "msg": "hand.reject", "opId": "c41d…", "reason": "forbidden" }
{ "msg": "hand.delta",  "meetingId": "mtg-7f3a", "epoch": 3, "ver": 4,
  "changes": [{ "op": "raised", "participantId": "p-maya", "order": 4, "by": "p-maya" }] }
{ "msg": "hand.snapshot", "meetingId": "mtg-7f3a", "epoch": 3, "ver": 4,
  "hands": [{ "participantId": "p-ana",  "order": 1, "raisedAt": "2026-10-29T17:40:02Z" },
            { "participantId": "p-raj",  "order": 2, "raisedAt": "2026-10-29T17:40:03Z" },
            { "participantId": "p-li",   "order": 3, "raisedAt": "2026-10-29T17:40:03Z" },
            { "participantId": "p-maya", "order": 4, "raisedAt": "2026-10-29T17:40:04Z" }] }

reason is forbidden, disabled or unavailable; only unavailable is worth retrying.

The one REST call: PATCH /meetings/{id}/options {reactionsEnabled, handRaiseEnabled}. It is an organizer's settings change, rare, and it belongs with the rest of meeting configuration, where HTTP authorization and audit already live. The meeting owner learns about the change from the config service and rejects those commands with disabled.

Message Direction Acked? Droppable? Size
reaction.send up No Yes about 100 B
reaction.burst down No Yes, after 1 s at most 300 B
hand.raise / lower / lowerAll up Yes, hand.ack or hand.reject No, retried with the same opId small
hand.delta down No, gaps are detected by ver No small
hand.snapshot down No No under 1 KB typical, 64 KB worst

The 300 B ceiling on reaction.burst comes straight from its shape: five counts, at most ten sender IDs at 16 B each, and a header. Notice what is missing from every upstream message: a sender. The server stamps participantId from the authenticated session at the gateway. A client cannot clap as someone else or lower another person's hand unless its session carries the organizer or presenter role. Identity is a property of the connection, not a field the client fills in.

Here is Maya's clap and Maya's hand on the same socket, side by side:

Sequence of Maya's clap and raised hand on one socket: reaction.send flows through the gateway to the meeting owner and comes back as an unacked reaction.burst to everyone, while hand.raise is written conditionally to the hand store and answered with hand.ack to Maya and hand.delta to all

The top half has no ack, no retry and no disk: Maya's client draws her clap locally, then receives the same room-wide frame as everyone else and subtracts her own clicks. The bottom half has an ack and a disk. That asymmetry is the module's one-line idea expressed as a protocol.

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.