Skip to content
noahkawaguchiPublic

About

Ranked choice voting serverless REST API

Resources

Stars

0 stars

Watchers

1 watching

Forks

Latest commit

 

History

154 Commits

Folders and files

Repository files navigation

English | 日本語

Verdict

🔗 Live interactive docs: verdict.noahkawaguchi.com


Verdict is a serverless REST API (with interactive Swagger UI) for working with ranked choice voting. Consumers can create polls, cast ballots, and retrieve results.

Instead of selecting a single choice, voters rank all choices in order of preference. The instant runoff algorithm then calculates a winner by repeatedly eliminating the last-place choice, using a recursive tie-breaking sub-poll when needed, and redistributing votes until one choice achieves a strict majority.

Table of Contents

  1. Tech Stack
  2. Architecture
  3. Project Structure
  4. Local Development
  5. Testing
  6. About Ranked Choice Voting

Tech Stack

Language Static Badge
Infrastructure Static Badge Static Badge Static Badge Static Badge
Docs Static Badge Static Badge

Architecture

The project is fully serverless, with API Gateway routing requests to a single Go Lambda function that reads from and writes to DynamoDB. Since there's no persistent server to manage, the cost scales to zero at rest.

Key design decisions:

  • Go: Go compiles to a small, self-contained binary with low memory usage and fast cold starts, making it a natural fit for Lambda.
  • Single Lambda function: All routes are handled by one function with an internal router, avoiding the overhead of managing multiple function deployments for a small API.
  • DynamoDB: A simple key-value access pattern (get poll by ID, get all ballots for a poll) maps cleanly to DynamoDB without needing a relational schema.

Project Structure

The design of the codebase generally follows the common three-layer pattern of API, domain, and infrastructure, but it allows for some pragmatic compromises due to the project's relatively small scale.

verdict/
├── cmd/              # Lambda entrypoint and config initialization
├── internal/
│   ├── api/          # HTTP routing and request handlers
│   ├── voting/       # Domain types and their associated logic
│   └── datastore/    # DynamoDB read/write
├── docs/             # Swagger UI static site (served via GitHub Pages)
├── template.yaml     # AWS SAM/CloudFormation infrastructure definition
└── justfile          # Project-specific commands and config (similar to Makefile)

Local Development

Prerequisite Installations

  • Go: The main language of the project.
  • Python: Only used to serve the static docs locally.
  • Docker: For the local Lambda/API Gateway emulator and DynamoDB container.
  • AWS SAM CLI: For local emulation of SAM infrastructure and deployment.
  • Just: Command runner used to save and run the commands used in development.

Running the Project Locally

  1. With the Docker daemon running, execute the following commands:
just       # Starts the Docker network, DynamoDB container, and API (port 3000)
just docs  # In a separate terminal, serves the Swagger UI docs (port 8000)
  1. View the interactive docs in a browser at http://localhost:8000.
  2. Use the "Servers" dropdown to select the http://localhost:3000 option.
  3. Use the "Try it out" functionality to hit the local endpoints.

All available recipes (commands) are documented in the justfile. (Run just --list to see a summary.)

Testing

just test     # Runs all tests
just test -v  # Runs all tests with verbose output

Tests cover all three packages (api, voting, datastore) and focus on the following strategies:

  • Dependency inversion and dependency injection: The api and datastore packages depend on interfaces rather than concrete implementations, making it straightforward to substitute mock implementations in tests.
  • Flexible mocking: Hand-rolled mocks use function fields with nil fallbacks, allowing each test case to inject only the behavior relevant to it without having to implement the full interface every time.
  • Table-driven tests: Tests are organized using Go's table-driven pattern to efficiently run multiple test cases through the same logic.
  • External test packages: All test files use external test packages (e.g. package api_test), testing public APIs as consumers use them rather than having access to internal implementation details.

About Ranked Choice Voting

Verdict implements a voting algorithm known as ranked choice voting or instant runoff voting. There are several variations, but this is the one used here:

Voting process

Instead of selecting a single choice, voters rank all choices in order of preference.

Determining a winner

Instead of immediately selecting a choice that has only a plurality of votes, the algorithm first checks if any choice has a strict majority of votes. If no choice does, the choice with the fewest votes is eliminated, and its votes are redistributed to the voters' next highest choices. This process of elimination continues until a single choice has a strict majority of votes.

What about ties for last?

In each round, the choice with the fewest votes is eliminated, but what if multiple choices are tied for last place? In this case, a sub-poll is simulated between only the tied choices. This is possible because voters provide a rank for every choice, so the algorithm can be used to determine their preferences among any subset of choices. If there is another tie for last place, the tie-breaking algorithm continues recursively.

While unlikely unless the numbers of choices and voters are very small, it is possible that multiple choices are tied for last place and received perfectly equivalent rankings. In this case, one of these lowest-ranking choices is eliminated by pseudorandom number generation.

About

Ranked choice voting serverless REST API

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages