Electronic Voting — Designed in Stages
You don’t need to design for scale on day one.
This guide is a staged design playbook: it tells you what to build at MVP, Growth, and Advanced scale for electronic voting so you don’t over- or under-build on anonymity, integrity, and verifiability.
When to use this: Use it when you’re designing or evolving an e-voting system (or similar ballot/tally system) from scratch or from an existing MVP; when you need voter registration, one vote per voter, and auditable tally. Skip or adapt if you only need informal polls with no anonymity or audit requirements.
Compared to what? Unlike designing for max assurance up front (e.g. E2E verifiability on day one), this adds complexity only when triggers appear. Unlike ad-hoc growth with no structure, you get a clear sequence: MVP (API + DB, one vote per voter, unlinkable ballot) → Growth (crypto, audit, role separation) → Advanced (E2E verifiability, distributed trust).
Stakes: If you over-build (e.g. distributed multi-authority before you need it), you pay in cost and crypto/ops risk. If you under-invest (e.g. no audit log when accountability is required), you hit compliance or trust issues. The stages tie additions to triggers so you avoid both.
Here we use electronic voting as the running example: voters, ballots, elections, and tally. The same staged thinking applies to any system that must preserve anonymity, integrity (no tampering, no double vote), verifiability (audit trail without revealing the vote), and availability. This is a sensitive domain; regulatory and legal requirements often dictate constraints beyond this technical outline.
Requirements and Constraints (no architecture yet)
Section titled “Requirements and Constraints (no architecture yet)”Functional Requirements
- Register voter — before election, eligible voters are registered (identity, eligibility); voter can authenticate when casting; registration and casting may be separate (e.g. different systems or phases).
- Cast ballot — voter submits one ballot per election; ballot contains choice(s); system records that this voter voted (for one-vote-per-voter) without storing the vote in linkable form; store ballot so it can be counted without revealing voter identity.
- Tally — after election closes, count votes; tally is deterministic from stored ballots; result (counts per option) is published; optional audit or proof.
Quality Requirements
- Anonymity — vote must not be linkable to voter in normal operation; no one (including operators) should be able to determine how a specific voter voted; separation of voter identity from ballot content.
- Integrity — no tampering: ballots cannot be altered after cast; no double vote: each voter at most one ballot; tally correctly reflects cast ballots; use cryptographic or procedural guarantees.
- Verifiability — audit trail that proves correctness (e.g. ballot count, tally computation) without revealing how anyone voted; optional: voter can verify their ballot was counted (voter-verifiable).
- Availability — system must be available during voting window; voters can cast and result can be tallied; consider resilience and recovery.
- Expected scale — number of voters, number of elections, geographic distribution, regulatory constraints.
Key Entities
- Voter — eligible participant; identity for registration and auth; after casting, no link to ballot content in tally path.
- Ballot — one submission per voter per election; contains encrypted or hashed choice(s); stored for tally; optionally signed or committed so it cannot be changed.
- Election — one contest; election_id, options (candidates or choices), open/close time; list of eligible voters or eligibility rule.
- Tally — aggregate result; counts per option; computed from ballots in trusted environment; published after election closes.
Primary Use Cases and Access Patterns
- Register voter — write path; add voter to roll (election-specific or global); auth credentials or tokens for casting; used to enforce one vote per voter.
- Cast ballot — write path; voter authenticates; check not already voted (by voter_id or token); accept ballot (encrypted or blinded); record “voter X has voted” (no link to content); store ballot for tally; idempotent so double-submit doesn’t count twice.
- Tally — read path; after close, load all ballots; compute counts (decrypt if needed, or homomorphic/sum); publish result; audit log of tally process (who ran it, when; not vote content).
Given this, start with the simplest MVP: API + DB, voter auth, cast ballot (store encrypted or hashed), one vote per voter (DB constraint), tally = aggregate in trusted environment—then add cryptographic or procedural separation (blind signature, homomorphic tally, mix-nets), audit log (who voted, not how), and separation of roles (registration vs casting vs tally) as trust and regulation demand.
Stage 1 — MVP (simple, correct, not over-engineered)
Section titled “Stage 1 — MVP (simple, correct, not over-engineered)”Goal
Ship a minimal correct e-voting flow: voters are registered and authenticated, each can cast one ballot (stored encrypted or hashed), and tally aggregates votes in a trusted environment. One API, one DB; strong one-vote-per-voter; no advanced crypto yet.
Components
- API — REST or similar; auth (voter credentials); register voter (admin or pre-registration); cast ballot (election_id, encrypted or hashed ballot payload); tally (admin, after close). Cast and tally are restricted (e.g. role or token).
- DB — voters (id, eligibility, auth info); elections (id, options, open_time, close_time); ballots (election_id, ballot_data encrypted or hashed, cast_at); voted (election_id, voter_id) to enforce one vote. Unique constraint on (election_id, voter_id) for voted; ballots stored without voter_id (or with anonymized token that cannot be linked back after submission).
- Cast ballot — voter authenticates; check (election_id, voter_id) not in voted; accept ballot (encrypt choice with election public key, or hash commitment); insert into ballots (no voter_id in ballot row, or use anonymous token); insert into voted (election_id, voter_id); return ack. Ballot table must not allow linking to voter; use separate voted table only for “has voted” check, and do not join to ballot content.
- Tally — run in trusted environment (admin or secure process); read all ballots for election; decrypt (if encrypted with election key) or open commitments; aggregate counts; publish result. Access control so only authorized tally process can decrypt.
- One vote per voter — DB unique constraint on (election_id, voter_id) in voted; application check before insert ballot; reject second cast for same election.
Minimal Diagram
Stage 1: one path in (voter + admin), one API, one DB; cast stores ballot (unlinkable) and voted; tally reads ballots in trusted environment.
Voter Admin | | v v+-----------------+| API (auth, cast, tally) |+-----------------+ | | v vAuth → Check voted → Store ballot (encrypted/hashed) | Insert voted | v vDB (voters, elections, voted, ballots) | ballots: no voter_id | v vOne row per voter in voted Tally: read ballots, decrypt, aggregatePatterns and Concerns (don’t overbuild)
- Anonymity: ballot table must not contain voter_id; only “voted” table has voter_id for enforcement; tally reads only ballots; separation is critical.
- Ballot storage: encrypt with election public key so only tally (with private key) can decrypt; or use hash commitment if opening is done in controlled tally step.
- Basic monitoring: cast count, duplicate attempt rate, tally run success; audit log for who ran tally.
Still Avoid (common over-engineering here)
- Blind signature, homomorphic tally, or mix-nets before you have a concrete audit or regulatory requirement for stronger verifiability.
- Separation of roles (registration vs casting vs tally) until a single trusted party is no longer acceptable (e.g. internal election vs public).
Why This Is a Correct MVP
- API + DB, voter auth, one vote per voter, ballot stored without link to voter, tally in trusted environment → minimal correct e-voting for controlled/trusted context; easy to reason about.
- Trust model: assume tally process and DB access are trusted; no advanced crypto yet; suitable for internal or low-stakes elections.
Stage 2 — Growth Phase (crypto, audit, separation of roles)
Section titled “Stage 2 — Growth Phase (crypto, audit, separation of roles)”You have a working MVP (API + DB, one vote per voter, ballot unlinkable to voter, trusted tally). Now one or more of the triggers below are true.
What Triggers the Growth Phase?
- Need stronger verifiability: when regulators or auditors require a verifiable tally or proof that no single party can link vote to voter → introduce cryptographic constructs (blind signature, homomorphic tally, or mix-net) so tally is auditable without breaking anonymity.
- Need audit trail: when you must prove who voted (eligibility) without revealing how they voted → add audit log (voter_id, “voted”, timestamp) separate from ballot store; auditors can verify ballot count matches voted count.
- Need separation of roles: when a single party holding both voter list and ballot content is no longer acceptable → split into registration (voter list, credentials), casting (accept ballot, check credential), tally (ballots only); no single DB has both voter_id and ballot content linkable.
Components to Add (incrementally)
- Cryptographic constructs — Blind signature: voter blinds ballot, gets authority to sign blinded form; unblinds to get signed ballot; authority never sees actual vote. Homomorphic tally: ballots encrypted with public key; tally sums ciphertexts (e.g. ElGamal) without decrypting individual ballots; result decrypted once. Mix-net: ballots shuffled and re-encrypted so order is lost; then decrypted; no link between input and output. Choose per regulation and threat model; e.g. blind signature for voter-issued credentials, homomorphic for privacy-preserving sum.
- Audit log — log “voter X cast a ballot at time T” (no choice); separate store from ballot table (e.g. append-only log or dedicated table); auditors verify count of voted records = count of ballots; retention per regulation (e.g. 7 years).
- Separation of roles — registration service: holds voter list, issues credentials or tokens for casting; casting service: accepts ballot + credential, checks credential via API to registration, records ballot and “voted”; tally service: receives only ballots (no voter ids), runs tally. No single DB has both voter_id and ballot content in linkable form; or use cryptographic separation so even DB admin cannot link.
Growth Diagram
Stage 2: we add registration, casting, and tally as separate services; each has API + auth + DB (or store); ballot store has no voter_id; audit log records “voted” only; tally reads ballots only.
Registration service Casting service Tally service API + auth API + auth API / batch (post-close) DB: voter list, DB: ballots (no Input: ballots only credentials voter_id), voted, (from casting) | | | v v v Voter gets credential Voter submits ballot Decrypt or open | (blinded/signed) commitments; aggregate v Store ballot Publish counts Casting checks Record "voted" (audit) credential via API No voter_id in ballotPatterns and Concerns to Introduce (practical scaling)
- Blind signature flow: voter creates ballot, blinds it, sends to authority; authority signs (sees only blinded form); voter unblinds; submits signed ballot to casting; casting verifies signature, stores ballot.
- Audit: independent auditor can verify (1) number of ballots = number of “voted” records, (2) tally computation on ballots matches published result; cannot see individual votes.
- Monitoring: cast success, credential validation failures, tally verification; security reviews.
Still Avoid (common over-engineering here)
- Full end-to-end verifiability (voter can check their vote in tally) until product and crypto expertise justify it.
- Distributed multi-authority until trust and regulation require it.
Stage 3 — Advanced Scale (E2E verifiability, distributed trust)
Section titled “Stage 3 — Advanced Scale (E2E verifiability, distributed trust)”You have separation of roles, audit log, and cryptographic tally. Now you need end-to-end verifiability or distributed trust (no single authority).
What Triggers Advanced Scale?
- Need end-to-end verifiability: when voters or auditors must verify that each ballot was counted (and that tally is correct) without revealing votes → add cryptographic proofs (e.g. zero-knowledge proofs, verifiable shuffle) so ballot commitments and tally proof are publicly verifiable.
- Need distributed trust: when no single authority can hold decryption keys or run tally → split election key among trustees (e.g. threshold ElGamal); tally requires threshold cooperation; key ceremony and trustee management.
- Need coercion resistance: when vote buying or coercion is a real threat → optional designs (e.g. voter can re-cast to invalidate previous ballot, or time-limited re-cast); audit without breaking anonymity; domain-specific.
Components to Add
- End-to-end verifiability — ballot accompanied by cryptographic proof that it is well-formed (valid choice); tally process produces proof that result is correct given ballots; voter can check their ballot appears in public list (as commitment) and that tally proof is valid; no one can see how voter voted from public data. Use established schemes (e.g. Helios-style commitments + ZK proofs).
- Distributed trust — election key split among trustees (e.g. threshold ElGamal, 2-of-3 or 3-of-5); no single party can decrypt; tally requires threshold cooperation; key ceremony, secure storage, and recovery procedures.
- Resilience to coercion — optional: allow voter to “overwrite” or invalidate previous ballot (e.g. cast again with null vote) so coercion cannot be verified; or time-limited re-cast; design depends on threat model.
- Audit without breaking anonymity — public audit trail: list of ballot commitments, tally proof; anyone can verify counts; no link to voter; optional dispute period (e.g. 48–72 hours before final result).
Advanced Diagram (conceptual)
Stage 3: same building blocks (API, auth, stores) as Growth, plus ZK proofs and multi-party tally; casting stores commitment (no opening); tally is multi-party (threshold decrypt or homomorphic sum); public commitments and tally proof for anyone to verify.
Voter Casting Tally (multi-party) (client) API + auth Trustee APIs + threshold Store: commitments protocol; input: ballot (no opening), audit commitments | | | v v vCreate ballot + ZK proof Verify proof, store Threshold decrypt | commitment or homomorphic sum v v vSubmit (ballot, proof) Publish commitment Publish result + proof | (no opening) | v v vLater: verify commitment Audit: count commitments Anyone verifiesin list, verify tally proof = count votes tally proofPatterns and Concerns at This Stage
- Zero-knowledge proofs: ballot encryption + proof that plaintext is valid choice; complex to implement correctly; use established libraries or schemes (e.g. zk-SNARKs, Bulletproofs for range proofs).
- Trustee management: key generation and distribution; secure storage; recovery procedures; legal and procedural (e.g. key ceremony with multiple witnesses).
- SLO and compliance: availability during vote window; retention and disposal of data per regulation; penetration testing and audits.
Still Avoid (common over-engineering here)
- End-to-end verifiability with custom crypto before you have both product demand and crypto expertise; use proven schemes first.
- Distributed multi-authority (e.g. 5+ trustees) until regulation or threat model explicitly requires it; start with threshold 2-of-3 if you need distributed trust.
- Coercion-resistance features (re-cast, receipt-freeness) until threat model includes vote buying or coercion; they add significant complexity.
Summarizing the Evolution
Section titled “Summarizing the Evolution”MVP delivers electronic voting with API + DB, voter auth, one vote per voter, ballot stored encrypted or hashed without voter link, and tally in trusted environment. That’s enough for controlled or low-stakes use.
As you grow, you add cryptographic constructs (blind signature, homomorphic tally, or mix-net), audit log (who voted, not how), and separation of roles (registration, casting, tally). You strengthen integrity and verifiability without breaking anonymity.
At advanced scale, you add end-to-end verifiability, distributed trust (multi-party tally), and optional coercion resistance. You meet high assurance and regulatory requirements; design and implementation require crypto and legal expertise.
This approach gives you:
- Start Simple — API + DB, one vote per voter, ballot unlinkable to voter, trusted tally; ship and learn in low-stakes context.
- Scale Intentionally — add crypto and role separation when trust and regulation demand it; add audit when accountability is required.
- Add Complexity Only When Required — avoid E2E verifiability and distributed trust until assurance and regulation justify them; get anonymity and integrity right first.
Example: internal board election → regulated shareholder vote
Stage 1: Single API + DB; voters authenticate; cast stores encrypted ballot and voted row (no voter_id in ballot table); tally runs in trusted env (admin). Enough for internal or low-stakes election. Stage 2: When auditors require “prove who voted without revealing how,” add audit log (voter_id, voted, timestamp) and optionally blind signature or homomorphic tally; when no single party can hold both voter list and ballots, split into registration, casting, and tally services. Stage 3: When shareholders or regulators require “anyone can verify the count,” add E2E verifiability (ballot commitments + tally proof); when keys cannot live with one authority, add threshold trustees and distributed tally.
Limits and confidence
This approach fits e-voting and ballot/tally systems where anonymity, integrity, and verifiability are central; adjust if your domain has different constraints (e.g. non-secret ballots). Use it as a heuristic and tie additions to real triggers, not a fixed spec; legal and regulatory requirements may override.
What do I do next?
- Capture your requirements using the sections above (functional, quality, entities, access patterns).
- Map your current system to Stage 1, 2, or 3 (MVP vs growth vs advanced).
- If you’re in Growth or Advanced, pick one trigger that applies and add the corresponding components first (e.g. audit log before full role separation).