Back to System Design
System Design
hard
mid

How would you design a poll widget that can be embedded in any app?

Embeddable widget showing a question + options; user picks one, sees results with percentages and bar visuals. Optimistic UI on vote; idempotent vote (one per user/session, server-enforced); show results after voting or after deadline; real-time updates via polling or WebSocket; handle anonymous vs authed users; rate limit; embed via iframe or script.

4 min read·~25 min to think through

A poll widget is small but touches state, optimistic UI, idempotency, real-time updates, and embedding — a compact system design question.

1. Requirements

  • Show a question with N options.
  • User votes once; sees results.
  • Results stay accurate as others vote (real-time or near).
  • One vote per user / session — server-enforced.
  • Embeddable on third-party sites.
  • Anonymous + authenticated users.

2. Data model

ts
Poll  { id, question, options: [{id, text}], createdAt, closesAt }
Vote  { pollId, optionId, voterKey, ts }    // voterKey = userId or device hash

Aggregate counts can be a denormalized field on the Poll for fast reads.

3. UI states

unloaded → loading → unvoted → submitting → voted (with results) → closed. Also error from any state.

4. Voting — optimistic with reconciliation

js
function vote(optionId) {
  const prev = state;
  setState(applyOptimisticVote(state, optionId));  // bump count, mark voted
  fetch(`/polls/${id}/vote`, { method: "POST", body: JSON.stringify({ optionId }), headers: { "Idempotency-Key": clientUuid }})
    .then(res => res.ok ? syncFromServer() : setState(prev));
}

Idempotency key prevents double-counting on retry. The server checks (pollId, voterKey) uniqueness regardless.

5. Identity

  • Authenticated — userId is the voterKey; strongest dedupe.
  • Anonymous — a stable device fingerprint or cookie; weaker. Combine with rate limiting and IP/session checks.

6. Real-time results

Three tiers:

  • PollsetInterval every N seconds while widget is visible (use IntersectionObserver to pause when off-screen).
  • Server-Sent Events — server pushes count updates; simpler than WS.
  • WebSocket — bi-directional; overkill unless the widget is also a chat.

For most polls, SSE or 5–10s polling is plenty.

7. Embedding

  • iframe — isolated, safe, no style bleed; can resize via postMessage.
  • Script tag — drop a <script> that injects markup; faster but exposes host page to widget bugs and vice versa.

iframe is the default for third-party embed; script for trusted hosts wanting native styling.

8. Anti-abuse

  • Rate limit by IP.
  • Server-side captcha or invisible challenge for anonymous votes.
  • Closed-poll enforcement on server.
  • Don't trust client timestamps.

9. Accessibility

  • Radio group semantics (role='radiogroup').
  • Results announced via a polite live region.
  • Bars labelled with percentages, not just visual length.

10. Performance

  • Tiny bundle for the embed.
  • SSR/CSR — embeds usually CSR; lazy-init when scrolled into view.
  • Cache poll metadata; only counts are hot.

Interview framing

"A poll widget is mostly about the vote semantics and the result-sync pattern. Voting is optimistic with an idempotency key so a retry can't double-count; the server enforces one-per-voter-key (userId for authed, device hash for anonymous). Results sync via SSE or 5–10s polling — paused with IntersectionObserver when offscreen. Embed via iframe for isolation; script for trusted hosts. Anti-abuse — rate limit by IP and add a challenge for anonymous votes. Accessibility — radio group + a live region for result announcements."

Follow-up questions

  • Why optimistic UI for voting, and how do you reconcile failures?
  • How do you prevent double voting from anonymous users?
  • iframe vs script tag for embedding — trade-offs?
  • How do you keep results in sync without overloading the server?

Common mistakes

  • No idempotency — double votes on retry.
  • Trusting client-side 'has voted' flags only.
  • Constant polling regardless of visibility.
  • Script-tag embed leaking styles into the host page.

Performance considerations

  • Aggregate counts on the server avoid scanning votes per request. Pause polling when widget is hidden. SSE is cheaper than WS for one-way updates.

Edge cases

  • User clears cookies and tries to revote.
  • Network failure between optimistic vote and confirmation.
  • Poll closes between rendering and voting.
  • Same user across devices.

Real-world examples

  • Twitter polls, YouTube community polls, Slido / Mentimeter live polls.

Senior engineer discussion

Seniors design optimistic UI with idempotency, enforce dedupe on the server (don't trust the client), pick the cheapest real-time mechanism that meets the requirement (often SSE or polling), and isolate embeds via iframe by default.

Related questions