•CASE STUDY

Voting System APIs and Data Model

4 min read·626 words·Beginner

Asked at

1 candidate report in May 2026

How to use this case study

SDE-2 / Mid

  • Clarify the vote type first (identified or anonymous, single or multiple choice, changeable or final)
  • Then design create poll
  • Cast vote and results APIs

SDE-3 / Senior

  • Enforce one vote per user with constraints and idempotency
  • Support changing votes
  • Count results at scale (sharded counters)
  • Hide partial results if required

Staff / Principal

  • Discuss fraud prevention
  • Anonymity guarantees vs verification
  • Auditability
  • High-traffic events

Problem RestatementProblem

Apple asked: design the APIs and core data model for a voting system. The hint: start by clarifying the type of vote, because it changes the design:

  • Identified vs anonymous: do we store who voted for what?
  • Single-choice vs multiple-choice (or ranked)?
  • Mutable vs final: can a voter change their vote before the poll closes?
  • Partial results: visible while voting is open, or only after it closes?

Let's design for a common case: identified voters, single or multiple choice (configurable), changeable until close, results hidden until close (configurable), and explain the variants.

Data ModelData model

CREATE TABLE polls (
  poll_id BIGINT PRIMARY KEY, title TEXT, created_by BIGINT,
  max_choices INT DEFAULT 1,          -- 1 = single choice
  allow_change BOOLEAN DEFAULT TRUE,
  results_visibility TEXT,            -- 'live' | 'after_close'
  opens_at TIMESTAMP, closes_at TIMESTAMP, status TEXT
);
CREATE TABLE options (option_id BIGINT PRIMARY KEY, poll_id BIGINT REFERENCES polls, label TEXT, position INT);
CREATE TABLE ballots (                 -- one ballot per voter per poll
  poll_id BIGINT, voter_id BIGINT, choices BIGINT[], version INT,
  cast_at TIMESTAMP, updated_at TIMESTAMP,
  PRIMARY KEY (poll_id, voter_id)       -- enforces one vote per user
);
CREATE TABLE option_counts (poll_id BIGINT, option_id BIGINT, shard INT, votes BIGINT,
  PRIMARY KEY (poll_id, option_id, shard));

APIs

POST /v1/polls                         { title, options: [...], max_choices, allow_change, results_visibility, closes_at }  → 201
GET  /v1/polls/{id}                    → poll + options (+ results if allowed) + my_ballot
PUT  /v1/polls/{id}/ballot             { choices: [optionId, ...] }   Idempotency-Key  → 200 { ballot }
                                        400 too many choices / invalid option; 409 changes not allowed; 410 poll closed
DELETE /v1/polls/{id}/ballot           → 204 (withdraw, if allowed)
GET  /v1/polls/{id}/results            → { option_id: count, ... } | 403 until closed
  • PUT on "my ballot" is naturally idempotent: sending the same choices twice gives the same state. It also handles changing a vote cleanly.
  • The server validates: the poll is open (server time), the options belong to this poll, the number of choices ≤ max_choices, and no duplicates.

Deep Dive — Counting votes that can be changedDeep dive

Voters cast a ballot, some change their mind, and everyone watches a live result. The count must always equal the ballots.

Weak

Increment a counter per vote

On cast, increment the chosen option's counter.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  V["Voter changes their choice"] --> INC["Increment the new option"]
  INC --> OLD["The old option is never decremented"]
  OLD --> DRIFT["Total votes exceeds the number of voters"]
  V2["Voter double-taps"] --> TWO["Counted twice"]
  TWO --> NOAUDIT["No per-voter record - nothing to recount from"]

Counters without ballots cannot be reconciled or recounted. Every anomaly — a changed vote, a retry, a bug — becomes permanent, because there is no underlying record to recompute from.

Good

Store one ballot per voter

Upsert a ballot keyed by (poll_id, voter_id) and count by aggregating ballots.

Now correctness is achievable: one ballot per voter by construction, changes are an update, and the count can always be recomputed. The problem is the read path — aggregating millions of ballots on every results view is far too slow for a live result.

Best

Ballots as the truth, counts as a maintained cache

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
    V["Voter"] --> API["Ballot API"]
    API --> TX["One transaction: upsert the ballot, decrement the old option, increment the new"]
    TX --> DB[("ballots + option_counts")]
    DB --> RC["Results cache"]
    R["Results viewers"] --> RC
    HOT["Hot poll"] --> SHARD["Each option's counter split into N shards"]
    SHARD --> SUM["Read = sum of shards"]
    DB --> RECON["Periodic recount from ballots - corrects any drift"]
  • Ballot and counts move in one transaction. The decrement of the old choice and the increment of the new one happen with the upsert, so the counts can never disagree with the ballots — which is what the first rung could not promise and the second could not serve quickly.
  • Shard the counter for hot polls. Millions of concurrent votes on one option serialise on a single row; splitting it into N counters and summing on read removes the contention, at the cost of a slightly more expensive read.
  • Recount periodically from the ballots. Because ballots are the source of truth, drift from any cause is correctable — the counts are an optimisation, never the record.

Serve viewers from a short-TTL results cache. Results are read far more than they are written, and a second of staleness on a live tally is invisible — while reading the counters directly at that rate is not.

Variants and Security

  • Anonymous voting: separate "who has voted" (voter_id → voted flag, to prevent double voting) from "what was voted" (ballots without voter IDs). Changing votes then becomes impossible or needs special cryptographic receipts. Say this trade-off.
  • Fraud: require authentication, rate limits, bot detection, and for public polls maybe verified accounts or CAPTCHAs.
  • Audit: ballot history (versions) and a recount from the ballots table.

Wrap-UpWrap-up

Clarify the vote semantics first (identity, choice count, mutability, result visibility), then model polls, options, one ballot per (poll, voter) enforced by the primary key, and per-option counters. Cast and change votes with an idempotent PUT that validates choices and poll timing, and update the ballot and counters in one transaction (sharding counters for hot polls). Serve results from a cache according to the visibility setting, and explain how anonymous voting changes the model.

More Case Studies

Frequently Asked Questions

What is the Voting System APIs and Data Model system design question?

Voting System APIs and Data Model is a system design interview question asked at FAANG companies. It covers api design, databases, security and tests your ability to design scalable, production-ready systems. InterviewSkool's breakdown walks you through requirements, API design, architecture, and trade-offs.

Which companies ask the Voting System APIs and Data Model question?

Apple have reportedly asked variations of this question in system design interviews. The exact wording may differ, but the core design challenges remain the same.

How should I prepare for the Voting System APIs and Data Model interview question?

Start with the problem statement and scale estimates, then design the high-level architecture. Focus on the core components, data model, and API design. InterviewSkool's breakdown covers the full solution with mermaid diagrams and trade-off analysis to help you prep efficiently.

What level is the Voting System APIs and Data Model question?

This question is suitable for SDE-2, SDE-3, and Staff engineer interviews. The level guidance on this page provides specific tips for each level — SDE-2 candidates should focus on core architecture, while Staff engineers should discuss trade-offs, monitoring, and incremental rollouts.

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with InterviewSkool's AI interviewer.

Start System Design Interview →