Files
system-design-url-shortener/README.md
T

4.7 KiB

system-design-url-shortener

A URL shortener built the way a system-design interview asks you to design one, then actually run: base62 ids, a block-allocating id generator, scrambled 7-character codes, Redis cache-aside with negative caching, click counters, and a Redis token-bucket rate limiter written as one atomic Lua script.

Companion to System Design Interview for Java Developers: Building a URL Shortener with Spring Boot.

Versions

Component Version Verified against
Spring Boot 4.1.1 spring-boot-starter-parent metadata on Maven Central; Starting ... Spring Boot (v4.1.1) in the test log
Spring Framework 7.0.9 spring-beans-7.0.9.jar in the packaged app
Tomcat 11.0.24 startup log
JDK 25.0.4.1+1 (Temurin LTS); source level 21 java -version
PostgreSQL 16.15 select version() (H2 in PostgreSQL mode for the automated tests)
Redis 7.0.15 redis-server --version

Run it

# PostgreSQL on :5433 with an empty database `urlshort`, Redis on :6379
./scripts/run.sh                      # builds and starts the app on :8080
./scripts/demo.sh                     # curl transcripts 01-06 into output/
./scripts/id-generation-demo.sh       # transcripts 07-08 (starts its own instances on :8082 and :8083)
./scripts/test-report.sh              # 24 tests, one line each, into output/09-test-results.txt

mvn test needs neither PostgreSQL nor Redis: the inmemory profile swaps in H2, an in-memory cache and an in-memory token bucket. Time is injected (java.time.Clock), so TTL and refill tests never sleep.

Run with -Duser.timezone=UTC (the scripts do). The PostgreSQL JDBC driver sends the JVM's time zone to the server, and a legacy zone name such as Asia/Calcutta can be rejected with invalid value for parameter "TimeZone".

Endpoints

Method and path What it does Success Failures
POST /api/v1/urls Create a link. Body: {"url": "...", "alias": "optional", "ttlSeconds": optional} 201 + Location 400 invalid URL or alias, 409 alias taken, 429 rate limited
GET /{code} Redirect 302 + Location 404 unknown, 410 expired
GET /api/v1/urls/{code}/stats Target, click count, dates 200 404
GET /api/v1/debug/cache-stats Diagnostic, delete before shipping. Cache hits and misses 200

Profiles and settings

Setting Default Effect
profile snowflake off Snowflake ids instead of database-allocated blocks; codes become 9-11 characters
profile inmemory off (tests) H2 + in-memory cache and rate limiter, no Redis or PostgreSQL
app.rate-limit.capacity / refill-per-second 5 / 0.5 Token bucket per client address, on POST /api/v1/urls only
app.cache-ttl / app.negative-cache-ttl 1h / 60s Cache lifetime for hits / for "no such code"
app.node-id 1 Snowflake node (0-1023); must differ per instance

Layout

src/main/java/com/ankurm/urlshortener/
  api/         UrlController, request/response records, ApiExceptionHandler (RFC 9457 problem details)
  core/        UrlService, UrlRepository (JdbcClient), Base62, CodeScrambler (40-bit Feistel),
               IdGenerator, BlockIdGenerator, SnowflakeIdGenerator, ShortenerProperties
  cache/       UrlCache, RedisUrlCache, InMemoryUrlCache
  ratelimit/   RateLimiter, RedisTokenBucketRateLimiter (+ token_bucket.lua), InMemory..., RateLimitFilter
src/main/resources/  application.yml, schema.sql, token_bucket.lua
src/test/java/...    Base62Test, CodeScramblerTest, SnowflakeIdGeneratorTest, SnowflakeProfileTest,
                     UrlShortenerApiTest, RateLimitTest

Captured output (output/)

File What it shows
01-create-and-redirect.txt 201 with Location, then a 302 to the long URL
02-cache-aside-and-clicks.txt First redirect misses and fills Redis (TTL 3600), the next nine hit; click counter in Redis
03-negative-cache.txt Unknown code: first lookup misses, second is answered from the - marker with TTL 60
04-alias-and-validation.txt Alias conflict 409, ftp: rejected, reserved alias rejected
05-rate-limit.txt Burst of 5, then 429 with Retry-After, refill after 2.2 s, redirects never limited, the bucket as stored in Redis
06-expiry.txt A 2-second link: 302, then 410; the Redis TTL is capped at the link's remaining life
07-id-generation.txt Ids and codes; a restart skips the rest of a block; two instances never overlap
08-snowflake.txt The snowflake profile: 10-character codes from 57-bit ids
09-test-results.txt All 24 tests, generated from the Surefire XML report

License

MIT, see LICENSE.