Poll, Option, Vote, and VotingStrategy as the core classes, preventing duplicate votes, supporting single-choice vs ranked-choice as pluggable strategies, and tallying results incrementally rather than recomputing from scratch.
Published September 23, 2026
class Poll { String id; List<Option> options; VotingStrategy strategy; }
class Option { String id; String text; }
class Vote { String voterId; String pollId; List<String> rankedOptionIds; } // supports both single and ranked
interface VotingStrategy { Result tally(List<Vote> votes, List<Option> options); }
Modeling Vote.rankedOptionIds as a LIST (even for single-choice polls, where it just holds one ID) rather than a single field is what lets the same Vote class serve both single-choice and ranked-choice without a schema fork — the VotingStrategy interprets the list differently depending on the poll's configured strategy.
class Poll {
Set<String> votersWhoVoted = ConcurrentHashMap.newKeySet();
boolean recordVote(Vote vote) {
return votersWhoVoted.add(vote.voterId); // returns false if voterId was already present
}
}
A Set (backed by a concurrent-safe implementation for a real multi-request environment) is the natural structure here — add() returning false for an already-present voter ID gives duplicate-prevention as a single atomic check-and-insert, the same database-level-uniqueness discipline from Payment — Idempotency Implementation applied at the application/in-memory layer.
class SingleChoiceStrategy implements VotingStrategy {
public Result tally(List<Vote> votes, List<Option> options) {
Map<String, Integer> counts = new HashMap<>();
for (Vote v : votes) counts.merge(v.rankedOptionIds.get(0), 1, Integer::sum);
return new Result(counts);
}
}
class RankedChoiceStrategy implements VotingStrategy {
public Result tally(List<Vote> votes, List<Option> options) {
// instant-runoff: repeatedly eliminate the lowest first-choice option and
// redistribute those votes to each ballot's next-ranked choice, until one option has a majority
}
}
Single-choice tallying is a simple count; ranked-choice (instant-runoff) is a genuinely more complex iterative elimination process — keeping them as separate VotingStrategy implementations behind one interface means Poll never needs to know or branch on WHICH voting method is in play, and adding a THIRD method (approval voting, say) is purely additive.
// naive: re-tally ALL votes on every single new vote — O(n) work per vote, O(n^2) total
// better for single-choice: maintain a running count, updated incrementally per vote
void onNewVote(Vote vote) {
runningCounts.merge(vote.rankedOptionIds.get(0), 1, Integer::sum); // O(1) per vote
}
For single-choice polls, incremental tallying (updating a running count map on each new vote, rather than re-scanning every vote cast so far) turns an O(n) per-vote operation into O(1) — a meaningful difference at real scale with a popular, actively-voting poll. Ranked-choice's instant-runoff algorithm is genuinely harder to make fully incremental (eliminating a candidate and redistributing votes doesn't cleanly decompose into per-vote updates), so ranked-choice polls more commonly re-tally on each REQUEST for results (not each vote), accepting a batched/on-demand freshness model rather than true real-time incremental updates.
Q: How would you prevent a single voter from voting via multiple accounts? A: This is fundamentally an identity/fraud problem outside the voting system's own data model — it needs to be solved at the AUTHENTICATION layer (Authentication System at Scale's account-integrity concerns), not the voting logic itself, which can only guarantee 'one vote per voterId,' not 'one vote per real person.'
Q: Does the duplicate-vote Set need to be persisted, or is in-memory sufficient? A: It needs to be PERSISTED (backed by a database with a unique constraint, mirroring Payment — Idempotency Implementation's approach) for any poll that matters beyond a single server's uptime — an in-memory-only Set loses its duplicate-prevention guarantee entirely if the server restarts or if the poll is served by multiple server instances that don't share state.
Q: Can a poll's VotingStrategy be changed after votes have already been cast? A: This should be explicitly disallowed by the design — changing tallying rules mid-poll would produce a result inconsistent with what voters were told they were voting under; the strategy should be locked in at poll creation and treated as immutable for that poll's lifetime.
Q: How does this connect to Design a Recommendation Engine's ranking concerns, given both involve 'scoring' options? A: They're conceptually related but solve different problems — a recommendation engine ranks based on PREDICTED preference (a model's output); a voting system tallies based on EXPRESSED preference (actual votes cast) — the underlying data structures for holding and aggregating scores can look similar, but the voting system's tally must be exactly reproducible and auditable, a much stricter correctness bar than a recommendation ranking's approximate, model-driven scores.