Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

URL Shortener API

Scalable URL shortener built with Laravel 8, designed for high read volume using Redis, async visit counters, and queue workers. Runs entirely in Docker — no local PHP or database setup required.

Architecture

Client → Nginx → Laravel (PHP-FPM) [horizontally scalable]
                    ├─ POST /api/shorten  → MySQL + Redis + Queue
                    ├─ GET  /api/top      → MySQL (cached 60s)
                    └─ GET  /{code}       → Redis string lookup + async visit counter

Queue worker  → CrawlUrl (fetch page title)
Scheduler     → flush pending visit counts from Redis to MySQL every minute
Component Role
Nginx Reverse proxy (port 8080), FastCGI to scaled app tasks
App Laravel API + redirects (scale with --scale app=N)
MySQL 8 Persistent URL storage
Redis 7 Redirect strings, visit counters, queue
Queue Background title crawling
Scheduler Flushes visit counters every minute

Hot path (redirect)

  1. Lean middlewareGET /{code} uses a dedicated redirect group (no session, cookies, or CSRF).
  2. Redis stringurl:redirect:{code} holds the long URL (30-day TTL); MySQL only on cache miss.
  3. Async visitsINCR + SADD in a Redis pipeline; scheduler flushes to MySQL.

See docs/load-testing.md for capacity findings and stress-test methodology.

Quick start (Docker only)

git clone https://github.com/fellipesg/url-shortener.git
cd url-shortener

docker compose up -d --build

Recommended Docker Desktop resources: at least 2 CPU / 2 GB RAM; 4 CPU / 4 GB for meaningful load tests.

Scale PHP-FPM horizontally:

docker compose up -d --build --scale app=3
# or: make scale REPLICAS=3

App: http://localhost:8080

Configuration: .env.docker (copied to .env inside the container on first boot).

API

Create short URL

curl -s -X POST http://localhost:8080/api/shorten \
  -H "Content-Type: application/json" \
  -d '{"url":"https://en.wikipedia.org/wiki/Genghis_Khan"}'

Rate limit: 30 requests/minute per IP on /api/shorten.

Redirect

curl -I http://localhost:8080/a1b2c3d4

Returns 302. Visits are counted asynchronously in Redis and flushed to MySQL every minute.

Top 100 URLs

curl -s http://localhost:8080/api/top

Cached for 60 seconds. Global API throttle: 60 requests/minute per IP (by design).

Useful commands

make up          # docker compose up -d --build
make test        # phpunit inside container
make stress-seed # 50k URLs + Redis warm
make scale REPLICAS=3

docker compose logs -f app nginx
docker compose down -v   # reset volumes

What changed (scalability pass)

Area Change
Routes API on routes/api.php; redirect on routes/redirect.php with minimal middleware
Services UrlShortenerService, ShortCodeGenerator, VisitCounter, UrlShortenerRepository
Redis Raw string redirect cache; pipelined visit counters; /top cache invalidation on create
Docker PHP 8.2-FPM, OPcache, tuned nginx, compose stack (mysql, redis, queue, scheduler)
FPM Static pool pm.max_children=16 (sized for ~4 GB host; was 64)
Scale docker compose --scale app=N; migrations guarded by MySQL GET_LOCK
Tests Feature tests + StressTestSeeder + stress runners under tools/stress/

Architecture findings (summary)

Load tests showed that methodology and host resources matter more than Laravel version for this workload.

  1. Firehose tests mislead — firing 300k requests with high concurrency and 10s curl timeouts produced <1% success on a 1 GB Docker VM, even after code tuning. That measured saturation, not steady-state capacity.
  2. Resources were the silent killer — with 4 CPU / 4 GB and fair sustained RPS tests, 1× app achieved 200 redirect RPS at 100% success (p50 ~28 ms) and a ceiling of ~240 RPS.
  3. More replicas ≠ faster on one VM--scale app=3 on a single Docker Desktop machine hurt throughput at 200–400 RPS (three FPM pools competing for the same cores).
  4. Easy wins that helped — lean redirect middleware, Redis string hot path, Redis pipeline for visits, FPM/nginx/OPcache tuning, php-fpm -F for container stability.
  5. /api/top at 50 RPS — ~4% success is expected: throttle:api (60/min) returns 429, not a capacity bug.
  6. Laravel upgrade — would help maintenance and PHP version, but would not replace edge redirect (OpenResty/Lua) or proper host sizing for millions of redirects/month.

Full tables, JSON artifacts, and Cursor canvas paths: docs/load-testing.md.

Scalability design (target: millions/month)

  • Redis redirect cache on the hot path
  • Deferred visit counters (Redis → batch MySQL flush)
  • Redis queue for title crawling
  • Cached /api/top (60s)
  • Indexes: unique short_url, index on visits
  • Rate limits on create and API

Future (out of scope here): redirect at the edge (nginx/OpenResty + Redis), read replicas, sharding, multi-region.

Project layout

app/
├── Console/Commands/StressLoadCommand.php
├── Http/Controllers/UrlShortenerController.php
├── Jobs/CrawlUrl.php
├── Repositories/UrlShortenerRepository.php
└── Services/
docker/
├── nginx/
└── php/
docs/
├── load-testing.md
└── canvas/          # Cursor canvas snapshots (open beside chat in Cursor IDE)
tools/stress/
storage/stress/      # committed JSON results from load runs

License

MIT

About

URL shortener API built with PHP

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages