Frontend system design: design a Poll widget
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.
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
Poll { id, question, options: [{id, text}], createdAt, closesAt }
Vote { pollId, optionId, voterKey, ts } // voterKey = userId or device hashAggregate 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
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:
- Poll —
setIntervalevery 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.