Challenges⚡ Real-TimeReal-Time Poll Widget
ðŸĶ–DinosaurSystem DesignReal-TimeVisualization

Real-Time Poll Widget

Design a poll widget with live vote updates, result visualization, anti-fraud measures, and embeddability — covering real-time sync, optimistic updates, and component isolation.

Real-Time Poll Widget

Challenge: "Design an embeddable poll widget that shows live results as votes come in."

Requirements

Functional

  • Display question with multiple choice options
  • Vote by clicking an option (one vote per user)
  • Show live results with animated bar chart
  • Real-time vote count updates via WebSocket/SSE
  • Embeddable in third-party sites via iframe or web component
  • Support multiple simultaneous polls

Non-Functional

  • Sub-100ms optimistic vote feedback
  • Handle 10,000+ concurrent voters
  • Prevent duplicate votes (fingerprinting + auth)
  • < 50KB widget bundle size
  • Accessible (keyboard, screen reader)

Architecture

┌─────────────────────────────────────────┐
│           Host Page                      │
│  ┌───────────────────────────────────┐  │
│  │  Poll Widget (iframe / WC)        │  │
│  │  ┌──────────────────────────┐     │  │
│  │  │ Question: "Best FW?"     │     │  │
│  │  │ ○ React     [████░] 42%  │     │  │
│  │  │ ● Vue       [██░░░] 31%  │     │  │
│  │  │ ○ Angular   [█░░░░] 17%  │     │  │
│  │  │ ○ Svelte    [░░░░░] 10%  │     │  │
│  │  │       Total: 1,247 votes │     │  │
│  │  └──────────────────────────┘     │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘
        ↕ postMessage / SSE
┌─────────────────────────────────────────┐
│           Poll API Server                │
│  WebSocket/SSE → vote broadcast         │
│  REST → create poll, cast vote          │
│  Redis → vote counts, dedup             │
└─────────────────────────────────────────┘

Data Model

interface Poll {
  id: string;
  question: string;
  options: PollOption[];
  totalVotes: number;
  createdAt: string;
  expiresAt?: string;
  settings: {
    allowMultiple: boolean;
    showResultsBeforeVote: boolean;
    requireAuth: boolean;
  };
}
 
interface PollOption {
  id: string;
  text: string;
  votes: number;
  percentage: number;
}
 
interface VoteEvent {
  type: 'vote';
  pollId: string;
  optionId: string;
  newCount: number;
  totalVotes: number;
}

Key Design Decisions

1. Real-Time Update Strategy

Server-Sent Events (SSE) over WebSocket for this use case:

  • Votes are unidirectional (server → client broadcast)
  • SSE auto-reconnects, works through HTTP proxies
  • Lower overhead than WebSocket for one-way data
function usePollUpdates(pollId: string) {
  const [poll, setPoll] = useState<Poll | null>(null);
 
  useEffect(() => {
    const source = new EventSource(`/api/polls/${pollId}/stream`);
 
    source.addEventListener('update', (event) => {
      const data: VoteEvent = JSON.parse(event.data);
      setPoll(prev => prev ? {
        ...prev,
        totalVotes: data.totalVotes,
        options: prev.options.map(opt =>
          opt.id === data.optionId
            ? { ...opt, votes: data.newCount, percentage: (data.newCount / data.totalVotes) * 100 }
            : { ...opt, percentage: (opt.votes / data.totalVotes) * 100 }
        ),
      } : null);
    });
 
    return () => source.close();
  }, [pollId]);
 
  return poll;
}

2. Optimistic Vote

function castVote(pollId: string, optionId: string) {
  setPoll(prev => {
    const totalVotes = prev.totalVotes + 1;
    return {
      ...prev,
      totalVotes,
      options: prev.options.map(opt => ({
        ...opt,
        votes: opt.id === optionId ? opt.votes + 1 : opt.votes,
        percentage: ((opt.id === optionId ? opt.votes + 1 : opt.votes) / totalVotes) * 100,
      })),
    };
  });
  setHasVoted(true);
 
  fetch(`/api/polls/${pollId}/vote`, {
    method: 'POST',
    body: JSON.stringify({ optionId }),
  }).catch(() => {
    setHasVoted(false);
    refetchPoll();
  });
}

3. Embeddability

Option A: iframe (strongest isolation)

<iframe src="https://polls.example.com/embed/abc123" width="400" height="300" />

Option B: Web Component (lighter weight)

<script src="https://polls.example.com/widget.js"></script>
<poll-widget poll-id="abc123"></poll-widget>

4. Anti-Fraud

  • Authenticated users: One vote per user ID
  • Anonymous: Fingerprint hash (IP + User-Agent + canvas fingerprint), stored in Redis with TTL
  • Rate limiting: Max 1 vote per poll per fingerprint per 24h
  • Client-side: Set cookie/localStorage flag (easily bypassed, but prevents casual duplicates)

Animated Result Bars

.result-bar {
  height: 40px;
  background: var(--poll-color);
  border-radius: 4px;
  transition: width 400ms cubic-bezier(0.4, 0, 0.2, 1);
}
 
@media (prefers-reduced-motion: reduce) {
  .result-bar { transition: none; }
}

Accessibility

  • Options are a role="radiogroup" before voting
  • After voting, results show with aria-valuenow on bars
  • Live vote count announced via aria-live="polite"
  • Full keyboard navigation (Tab between options, Enter to vote)
  • Color is not the only differentiator (percentages shown as text)