Building an Atomic, High-Throughput Election System: A System Design Case Study

It’s election night. Millions of citizens have cast their votes, and across the country, hundreds of thousands of ballot boxes are being counted simultaneously. Broadcasters, news websites, and political parties all want instant access to the vote totals — both nationally and per city. Every second matters, and delays in reporting can lead to confusion or mistrust. Building a system to handle this scenario requires balancing speed, concurrency, and reliability. The system must aggregate thousands of updates per second using atomic operations, provide real-time visibility for dashboards and live feeds, and at the same time maintain a persistent history for auditing and post-election analysis.

White Ballot Box on White Background by pexels

In this post, we’ll explore a solution that uses Redis for fast and atomic, high level real-time vote counting and PostgreSQL for durable logging. This hybrid approach allows us to deliver instant insights while keeping complete historical records, demonstrating a pattern that is widely applicable to other high-concurrency, real-time systems.


Visit Deep Learning Enabled Art Exhibition: Digital Van Gogh




Real-Time Vote Counting System

To handle real-time vote counting effectively, we use a hybrid architecture: Redis for fast counters for high level results and PostgreSQL for persistent logging. This allows us to deliver instant national and city-level totals while maintaining full historical records.

Redis for Atomic Fast Counters

Each political party and city combination has its own Redis key. When a ballot box reports results, we bulk update the counters using INCRBY:

votes = {
    "tories": 250,
    "labour": 200,
    "libdems": 45,
    "reform": 20,
}
city = "london"

for party, count in votes.items():
    # Increment national total
    redis.incrby(f"election:2025:party:{party}", count)
    # Increment city-level total
    redis.incrby(f"election:2025:city:{city}:party:{party}", count)

Key Naming Strategy:

  • National totals: election:2025:party:<PARTY_NAME>
  • City totals: election:2025:city:<CITY_NAME>:party:<PARTY_NAME>

This design has several benefits:

  • Atomic updates: Multiple ballot boxes can update totals concurrently without conflicts.
  • Instant querying: A simple GET retrieves the current vote count for a political party for country level or city level.
  • Scalability: Adding new cities or parties requires no schema changes.

Example: Three UK Cities

Let’s take London, Manchester, and Edinburgh with parties tories, labour, libdems, and reform. When ballot boxes report votes:

  • London: Tories 120, Labour 200, Lib Dems 45, Reform 20
  • Manchester: Tories 90, Labour 150, Lib Dems 30, Reform 10
  • Edinburgh: Tories 50, Labour 80, Lib Dems 40, Reform 5

The system updates both national and city-level counters in Redis, providing real-time totals for dashboards:

tories_total = redis.get("election:2025:party:tories")
labour_london = redis.get("election:2025:city:london:party:labour")

RDBMS for Persistent Logging

While Redis handles live counts, a relational database management system such as PostgreSQL stores detailed logs of every ballot box report:

ballot_id | city      | party    | votes | timestamp
-----------------------------------------------------
1001      | london    | tories   | 120   | 2025-09-15 18:12
1001      | london    | labour   | 200   | 2025-09-15 18:12

This allows:

  • Historical reporting per city or party
  • Auditability for election verification
  • Analysis of reporting patterns (e.g., which ballot boxes reported first)

Why Not Only RDBMS?

Instead Redis, it’s possible to rely solely on PostgreSQL using SELECT … FOR UPDATE to increment counters:

BEGIN;
SELECT votes FROM vote_count
WHERE party='tories' AND city='london' FOR UPDATE;
UPDATE vote_count SET votes = votes + 120
WHERE party='tories' AND city='london';
COMMIT;

However, this approach has serious drawbacks under high load:

  • Row-level locking creates contention with hundreds of concurrent updates
  • Real-time dashboards may lag
  • Each update requires a full transaction, it’s much slower than Redis atomic counters

Using Redis for real-time counting and PostgreSQL for persistent logging combines speed and reliability, making it suitable for large-scale, high-concurrency systems like elections.

Redis Cluster Sharding for Production

In a production-grade election system, a single Redis instance is not enough to handle high concurrency and large datasets. That’s why we use a Redis Cluster, which automatically spreads data across multiple nodes.

From the user or developer perspective, it’s very simple: every key behaves as if it’s in a single Redis instance. Whether you SET, INCRBY, or GET a key, the cluster automatically routes your request to the correct node behind the scenes. You don’t need to worry about which node actually stores the key — the client library handles it.





This setup ensures that real-time vote counts remain fast, reliable, and scalable, even when thousands of ballot boxes report results simultaneously.

Deferred Processing of Ballot Box Requests with Idempotency

In a high-concurrency election system, ballot box requests can sometimes fail due to network issues, worker crashes, or database errors. To handle this reliably, we use a deferred processing pattern with idempotency, storing the raw votes for all parties in the database.

Workflow

1- Store the request in the database: When a ballot box reports results, save the entire request as JSON:

id | ballot_box_id | city  | votes_json                                      | completed | created_at
------------------------------------------------------------------------------------------------
1  | BB123         | london | {"tories":120,"labour":200,"libdems":45,"reform":20} | false     | 2025-09-15 18:12

completed is initially set to false.

2- Process the request: For each party in votes_json, increment Redis counters using INCRBY and insert or update the Postgres logs. If both succeed, mark completed = true.

3- Retry for pending requests: If a request remains completed = false for more than 10 minutes, consider it pending and retry processing.

Redis increment and postgres update steps are idempotent: if they are already happened for a given request, skip it to avoid double transaction.

request = get_request_from_db(ballot_box_id)

if not request.completed:
    # Redis update
    for party, count in request.votes_json.items():
        redis_idempotency_key = f"processed:{request.id}:{party}:redis"

        if not rc.exists(redis_idempotency_key):
            rc.incrby(f"election:2025:party:{party}", count)
            rc.set(redis_idempotency_key, 1)

    # Postgres update
    postgres_idempotency_key = f"processed:{request.id}:postgres"
    if not rc.exists(postgres_idempotency_key):
        insert_vote_log(request)
        rc.set(postgres_idempotency_key, 1)

    # Mark as completed
    mark_completed(request)

Advantages

  • Reliable: Failed or stuck requests can be retried without affecting vote totals.
  • Idempotent: Each ballot box request is processed exactly once per party.
  • Scalable: Redis handles fast atomic counters, while Postgres maintains a durable audit trail.
  • Raw Data Storage: Storing the complete votes JSON allows for flexible reporting, analytics, and auditing.

Conclusion

Designing a real-time election result system highlights the importance of combining speed, scalability, and reliability. Using Redis for atomic counters allows us to aggregate votes instantly, whether at the national level or broken down by city. Even when hundreds of ballot boxes report results simultaneously, the system can safely increment totals without race conditions or delays.

At the same time, PostgreSQL provides a durable, persistent store for detailed logs, capturing which ballot box reported which votes and when. This ensures complete auditability and allows analysts to generate historical reports, verify results, and answer questions like “Which city reported the most votes between 6 PM and 7 PM?”

While it would be possible to rely solely on a relational database using SELECT FOR UPDATE to increment totals, this approach quickly becomes a bottleneck under high concurrency. Row-level locking slows down updates and makes real-time dashboards lag. By combining Redis for real-time counting with PostgreSQL for persistent logging, we achieve both speed and reliability.

This hybrid pattern is not only applicable to elections but also to any scenario that requires real-time metrics under high concurrency: live dashboards, active user counters, distributed job queues, and more. It’s a clean, scalable solution that balances performance with durability — exactly the kind of design interview solution that demonstrates practical understanding of system design principles.


Support this blog financially if you do like!

Buy me a coffee      Buy me a coffee


Leave a Reply