A high-performance, distributed URL shortener built as a system-design learning project, featuring Cassandra + Redis storage, real-time click analytics, a sliding-window rate limiter, and independently scalable read/write services.
Have a question?
·
Request Feature
This project was built using the Go programming language, following Clean Architecture principles (domain / use cases / infrastructure / delivery layers) with dependency injection throughout. The HTTP layer is powered by Hertz, chosen for its high-performance I/O and production-ready ergonomics. Data is persisted in Cassandra (durable source of truth) and cached in Redis (fast reads, real-time counters, and rate limiting via Lua scripting). HAProxy load-balances traffic across independently scalable read and write service groups. The project uses Docker and Docker Compose for containerization, k6 for load testing, and Go's built-in testing framework (with TDD and hand-written mocks) for unit tests. API documentation is generated with swag/Swagger.
This project intentionally splits reads and writes into separate services (a CQRS-style split), since redirects vastly outnumber URL creations in a real-world shortener:
write— handlesPOST /shorten. Generates a distributed, collision-free short code (Snowflake ID + Base62 encoding) and persists it to Cassandra, warming Redis on write.read— handlesGET /:short_code(redirect) andGET /stats/:short_code(click analytics). Reads cache-first from Redis, falling back to Cassandra on a miss.lb— HAProxy, routing/shortento writer replicas and everything else to reader replicas.redis— cache-aside layer for URLs and stats, plus the backing store for the sliding-window rate limiter (atomic via Lua scripting).cassandra— durable source of truth for URLs and click statistics.
Every use case depends only on small, interface-based repository ports — the caching strategy, Redis, and Cassandra are entirely invisible above the infrastructure layer.
For running this project, you will need the following:
- Go
- Docker
- Docker Compose
- GNU Make (usually pre-installed on Linux)
And you need to download the project to your computer.
After fulfilling all requirements, you need to install the dependencies:
go mod downloadThe fastest way to get the whole system running — API services, load balancer, Redis, and Cassandra — is with Docker Compose:
docker compose up --buildThis starts:
lb— HAProxy, exposed athttp://localhost:9999write-1/write-2— writer replicas (internal only, routed vialb)read— reader replicas (internal only, routed vialb)redis— exposed atlocalhost:6379cassandra— exposed atlocalhost:9042cassandra-init— one-shot keyspace/table initialization, runs once and exits
Once everything is healthy, the API is reachable at http://localhost:9999. For example:
curl -X POST http://localhost:9999/shorten \
-H "Content-Type: application/json" \
-d '{"long_url": "https://example.com"}'To stop everything:
docker compose downTo also remove persisted Redis/Cassandra data volumes:
docker compose down -vThe Makefile wraps common development and testing tasks:
| Command | Description |
|---|---|
make test |
Runs the full unit test suite (internal/ and pkg/). |
make covertest |
Runs all tests with coverage reporting. |
make racetest |
Runs the concurrent Snowflake ID generation test under Go's race detector, to verify no duplicate IDs are produced under concurrent access. |
make swag |
Regenerates the Swagger/OpenAPI documentation from source annotations. |
make smoke_test |
Runs a lightweight end-to-end sanity check (create → redirect → stats → 404) against the running Docker stack via k6. |
make write_test |
Runs a k6 load test against the write path (POST /shorten) at steady-state and peak QPS. |
make read_test |
Runs a k6 load test against the read path (redirects) at steady-state and peak QPS. |
make load_test |
Runs the combined read+write k6 load test, simulating realistic mixed traffic at the real-world read:write ratio. |
All *_test load-testing commands run k6 through Docker Compose's loadtest profile, so they never start automatically with docker compose up — they're opt-in and run against the already-running stack.
Interactive Swagger UI is available in non-production environments at:
http://localhost:9999/swagger/index.html
To regenerate the documentation after changing handler annotations:
make swagLoad tests are written in k6 and modeled on the following system-design targets:
- Writes: ~115 QPS steady state, ~350 QPS peak (3x)
- Reads: ~11,500 QPS steady state, ~35,000 QPS peak (3x)
Run a quick sanity check before any real load test:
make smoke_testThen run the isolated or combined load tests:
make write_test
make read_test
make load_testNote: sustaining the peak read target (35,000 QPS) requires meaningful hardware for both the k6 load generator and the backing services — a local single-node Cassandra/Redis setup is expected to bottleneck well below that number. The goal of these tests is to observe where the system saturates first, not to guarantee hitting the raw target on a laptop.
If you'd like to contribute to this project, please follow these steps:
- Fork this repository.
- Create a branch:
git checkout -b feat/your-feature. - Make your changes and commit them:
git commit -m 'Add some feature'. - Push to the original branch:
git push origin feat/your-feature. - Create a pull request.
Julio Martins
Made by Julio Martins 👋🏽 Contact me!