Inspiration
We wanted to build something deceptively simple — a URL shortener — and treat it like a real production system. The challenge wasn't the feature, it was the engineering: how do you make it reliable, fast, and scalable under real load?
What it does
URL Shortener — a production-grade link shortening service with a full REST API.
Users can submit any long URL and receive a short code (e.g. http://206.189.59.175:8000/aB3xZ9). Accessing the short link redirects instantly to the original URL. The system tracks every redirect as a click event, stores user and URL records, and exposes full CRUD endpoints for managing users, links, and events.
Core features:
- POST /shorten — generate a short URL with an auto-generated or custom short code
- GET / — redirect to the original URL (Redis-cached for speed)
- POST /urls — create a URL with title, user ownership, and metadata
- GET /users, POST /users — manage user accounts with email validation
- GET /events — query click events filtered by URL, user, or event type
- Automatic click tracking on every redirect
- Graceful Redis fallback — if cache is unavailable, falls back to the database
- Full input validation — rejects malformed requests, invalid emails, bad URL schemes
- GET /health — health check endpoint for monitoring
How we built it
We started with a minimal Flask app and iterated through three engineering tiers for both reliability and scalability:
Reliability track: Added a /health endpoint, built a GitHub Actions CI pipeline, wrote 100+ tests with 95% coverage, implemented structured error handling, and configured restart: always for chaos resilience.
Scalability track: Ran k6 load tests at 50, 200, and 500 concurrent users. Fixed bottlenecks at each stage — moving from a single Flask process to Gunicorn workers, adding Nginx load balancing across two app containers, adding connection pooling, and finally Redis caching for redirect lookups.
Deployment: Manually provisioned a DigitalOcean Droplet (CentOS 9), installed Docker, and ran the full stack via Docker Compose — live at http://206.189.59.175:8000.
Challenges we ran into
- Nginx crash-looping because it couldn't resolve app container DNS before they
fully joined the Docker network
- gevent async workers breaking psycopg2 due to C extension incompatibility — had to revert to sync workers
- PostgreSQL connection exhaustion under 200 VUs before we added connection pooling
- CI test failures caused by patching the wrong class (PostgresqlDatabase vs PooledPostgresqlDatabase)
Accomplishments that we're proud of
What we learned
We passed majority tests, that's no small feat
- How horizontal scaling with Nginx and multiple app instances actually works at the infrastructure level
- The difference between connection pooling and opening raw DB connections — and why it matters under load
- How Redis caching dramatically reduces database pressure on hot read paths
- How to provision and deploy a production server on DigitalOcean from scratch — no managed platforms, just a Linux VM and Docker
- How CI/CD gates (coverage thresholds, test requirements) enforce quality before code reaches production
What's next for PE hackathon 2026 - Production grade url shortener
review code and continuous learning
Built With
- actions
- digitalocean
- docker
- flask
- github
- gunicorn
- k6
- nginx
- peewee
- postgresql
- python
- redis
Log in or sign up for Devpost to join the conversation.