Low Level Design: Online Polling and Voting System

Introduction

Polling systems must ensure one vote per eligible voter, prevent fraud, aggregate results in real time, and optionally preserve anonymity. The design must handle concurrent vote submissions at scale while guaranteeing correctness and providing live result visibility to participants.

Schema

Core tables:

  • Poll (poll_id, creator_id, question, status ENUM draft/active/closed, start_time, end_time, vote_limit_per_user, is_anonymous, created_at)
  • Option (option_id, poll_id, text, vote_count)
  • Vote (vote_id, poll_id, option_id, voter_fingerprint, voted_at)

Vote Recording

When a voter submits a vote, the system first verifies the poll is active by checking that now falls between start_time and end_time. It then checks voter eligibility: authenticated sessions use the user account; anonymous sessions derive a fingerprint. An idempotency check confirms the voter has not already voted in this poll. If all checks pass, a Vote row is inserted and the corresponding option’s vote_count counter is incremented in Redis atomically.

Deduplication

For authenticated users, a unique constraint on (poll_id, voter_id) at the database level prevents duplicate votes. For anonymous users, a fingerprint is derived from the combination of IP address, browser fingerprint, and a session cookie, then hashed together with the poll_id. A Redis SET key keyed on hash(poll_id:fingerprint) with a TTL equal to the poll duration is written using SET NX (set if not exists) before inserting the Vote row. This prevents concurrent double submissions even under race conditions.

Result Aggregation

Vote counts are maintained in Redis: HyperLogLog per option for extremely high-frequency polls where approximate counts are acceptable, or exact integer counters for standard polls. Result queries return (option_id, count, percentage) for all options, with percentages recomputed on each read from the live Redis counters. When a poll closes, a reconciliation job computes final official results directly from the Vote table for audit integrity and stores them as the canonical result set.

Anonymity

For anonymous polls, the Vote record stores the voter fingerprint rather than a user_id. Because the fingerprint is a one-way hash, no voter can be identified from the Vote record alone. Auditors can verify that each fingerprint appears only once in the poll (enforcing the one-vote rule) without knowing who that fingerprint belongs to. For high-security elections, a blind signature scheme is used: the election authority signs a blinded token, the voter unblinds it and submits their vote with the signature, allowing the system to verify validity without linking the vote to the voter’s identity.

Fraud Prevention

Velocity limiting enforces a maximum of 100 votes per IP per hour across all polls, tracked in Redis with a sliding window counter. Suspicious IPs trigger a CAPTCHA challenge before their vote is accepted. An anomaly detection layer monitors for sudden vote spikes originating from a narrow subnet and flags them as potentially coordinated. Flagged votes are quarantined in a separate table and excluded from live counts pending manual review by the poll operator.

Real-Time Results

Clients subscribe to live results via Server-Sent Events at /polls/{id}/results/stream. On each accepted vote, the result server publishes an event to a Redis pub/sub channel for that poll. Subscribed result servers receive the event, recompute current percentages from Redis counters, and push an SSE event to all connected clients. Updates are debounced to a maximum of 10 per second per poll to prevent result servers from being overwhelmed during high-traffic voting windows.

Frequently Asked Questions: Online Polling and Voting System

How do you implement one-vote-per-user deduplication for anonymous polls?

For authenticated users, a voted_polls table keyed on (user_id, poll_id) with a unique constraint prevents duplicate submissions at the database level; the API checks existence before inserting. For anonymous polls, a device fingerprint (browser fingerprint + IP hash) or a signed cookie token is issued on first visit and stored as the voter identifier. Bloom filters or a Redis SET per poll can do fast membership checks before the database write. Note that anonymous deduplication is best-effort and can be circumvented; communicate this limitation in the poll UI.

How do you implement real-time poll result streaming with Server-Sent Events (SSE)?

Each poll has a results SSE endpoint (GET /polls/{id}/stream). When a client connects, the server registers the connection in an in-memory subscriber map keyed by poll_id. Vote submission publishes an increment event to a pub/sub channel (Redis pub/sub or an internal event bus). A fan-out worker subscribes to that channel, recomputes or updates the aggregate counts, and pushes a data: JSON event to all active SSE connections for that poll. Clients reconnect automatically using the Last-Event-ID header. For scale, SSE connections can be handled by a dedicated streaming service behind a load balancer with sticky sessions or a shared Redis pub/sub backend.

How do you detect and prevent vote fraud using velocity limits?

A rate-limiting layer (token bucket or sliding window counter in Redis) tracks vote attempts per IP, device fingerprint, and user account within configurable windows (e.g., max 1 vote per poll per device, max 10 poll creations per hour per account). Sudden spikes in votes from a narrow IP range trigger a CAPTCHA challenge or temporary block. Suspicious patterns (same fingerprint with rotating IPs, votes arriving in millisecond bursts) are flagged for async review. Confirmed fraudulent votes are soft-deleted without notifying the abuser, preserving audit trail.

How does a blind signature scheme enable anonymous voting in a polling system?

In a blind signature protocol the voter (1) blinds their ballot using a random blinding factor, (2) sends the blinded ballot to the authentication server, which signs it without seeing the content, (3) receives the blind signature and unblinds it to get a valid signature on the original ballot, then (4) submits the ballot plus signature to a separate vote-collection server that cannot link ballot to voter identity. The collection server only verifies the signature is from the auth server and that it has not been seen before (via a spent-token set). This decouples authentication from vote content, achieving both eligibility verification and ballot anonymity.

How do you finalize poll results when a poll closes?

Poll close is triggered either by a scheduled job that checks poll.end_time <= NOW() or by an explicit admin action. On close, the system atomically sets poll.status = CLOSED and enqueues a finalization job. The job aggregates raw vote records into a results snapshot table (poll_id, option_id, final_count, computed_at) which becomes the canonical result. The live vote table remains intact for audit. SSE connections receive a final event with type: closed and the result payload, then the server closes the stream. Subsequent reads serve the snapshot rather than querying the live votes table, ensuring consistency and fast response.

See also: Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering

See also: LinkedIn Interview Guide 2026: Social Graph Engineering, Feed Ranking, and Professional Network Scale

See also: Snap Interview Guide

Scroll to Top