System design URL shortener: Spring Boot 4.1.1, base62 ids, Redis cache-aside, Redis Lua token bucket, 24 tests and live transcripts

This commit is contained in:
2026-10-22 08:13:35 +00:00
commit aa61ed8786
51 changed files with 1717 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
$ curl -si -X POST /api/v1/urls -d '{"url":"https://ankurm.com/java-interview-preparation-guide/"}'
HTTP/1.1 201
X-RateLimit-Remaining: 4
Location: http://localhost:8080/6MelkkB
{"code":"6MelkkB","shortUrl":"http://localhost:8080/6MelkkB","expiresAt":null}
$ curl -si -X GET /6MelkkB
HTTP/1.1 302
Location: https://ankurm.com/java-interview-preparation-guide/
+15
View File
@@ -0,0 +1,15 @@
code = 7qqChen
$ redis-cli EXISTS url:7qqChen # before the first redirect
0
--- after 1st redirect (cache miss, database read, cache filled)
$ redis-cli GET url:7qqChen
https://example.com/cache-demo
$ redis-cli TTL url:7qqChen
3600
cache hits +0, misses +1
--- after 9 more redirects
cache hits +9, misses +1
$ redis-cli GET clicks:7qqChen
10
$ curl -s http://127.0.0.1:8080/api/v1/urls/7qqChen/stats
{"code":"7qqChen","longUrl":"https://example.com/cache-demo","clicks":10,...
+13
View File
@@ -0,0 +1,13 @@
$ curl -si -X GET /Zq9Zq9
HTTP/1.1 404
{"detail":"no such code: Zq9Zq9","instance":"/Zq9Zq9","status":404,"title":"Not Found"}
$ curl -si -X GET /Zq9Zq9
HTTP/1.1 404
{"detail":"no such code: Zq9Zq9","instance":"/Zq9Zq9","status":404,"title":"Not Found"}
cache misses: before=2 after-first-404=3 after-second-404=3
$ redis-cli GET url:Zq9Zq9 # the negative-cache marker
-
$ redis-cli TTL url:Zq9Zq9
60
+21
View File
@@ -0,0 +1,21 @@
$ curl -si -X POST /api/v1/urls -d '{"url":"https://example.com/one","alias":"demo-link"}'
HTTP/1.1 201
X-RateLimit-Remaining: 4
Location: http://localhost:8080/demo-link
{"code":"demo-link","shortUrl":"http://localhost:8080/demo-link","expiresAt":null}
$ curl -si -X POST /api/v1/urls -d '{"url":"https://example.com/two","alias":"demo-link"}'
HTTP/1.1 409
X-RateLimit-Remaining: 3
{"detail":"alias already taken: demo-link","instance":"/api/v1/urls","status":409,"title":"Conflict"}
$ curl -si -X POST /api/v1/urls -d '{"url":"ftp://example.com/file"}'
HTTP/1.1 400
X-RateLimit-Remaining: 2
{"detail":"only http and https URLs are allowed","instance":"/api/v1/urls","status":400,"title":"Bad Request"}
$ curl -si -X POST /api/v1/urls -d '{"url":"https://example.com","alias":"api"}'
HTTP/1.1 400
X-RateLimit-Remaining: 1
{"detail":"alias must be 4-32 chars of A-Z a-z 0-9 _ -","instance":"/api/v1/urls","status":400,"title":"Bad Request"}
+17
View File
@@ -0,0 +1,17 @@
capacity 5, refill 0.5 tokens/second (one every 2 s)
request 1 -> HTTP/1.1 201 X-RateLimit-Remaining: 4
request 2 -> HTTP/1.1 201 X-RateLimit-Remaining: 3
request 3 -> HTTP/1.1 201 X-RateLimit-Remaining: 2
request 4 -> HTTP/1.1 201 X-RateLimit-Remaining: 1
request 5 -> HTTP/1.1 201 X-RateLimit-Remaining: 0
request 6 -> HTTP/1.1 429 X-RateLimit-Remaining: 0 Retry-After: 2
request 7 -> HTTP/1.1 429 X-RateLimit-Remaining: 0 Retry-After: 2
$ redis-cli HGETALL rl:127.0.0.1
tokens
0.046000000000000082
ts
1792656760797
--- sleeping 2.2 s
request 8 -> HTTP/1.1 201 X-RateLimit-Remaining: 0
--- redirects are not limited: 30 GETs in a row
302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302
+5
View File
@@ -0,0 +1,5 @@
created ArYvqWu with ttlSeconds=2
t=0s -> 302
$ redis-cli TTL url:ArYvqWu # capped at the link's remaining life, not the 1 h default
2
t=3s -> 410
+24
View File
@@ -0,0 +1,24 @@
--- instance A: three links
id | code | len
----+---------+-----
1 | 6MelkkB | 7
2 | 7qqChen | 7
3 | J4Wn4LP | 7
(3 rows)
--- instance A stopped and restarted (ids 4..100 of its first block are never used)
id | code | len
-----+---------+-----
1 | 6MelkkB | 7
2 | 7qqChen | 7
3 | J4Wn4LP | 7
101 | IZKi2wO | 7
(4 rows)
--- two instances running at once never overlap: B starts while A is alive
id | code
-----+---------
102 | DDCfjej
201 | Globq13
(2 rows)
+10
View File
@@ -0,0 +1,10 @@
code=7sWzEmdQLw length=10
code=7sWzF1xlNQ length=10
code=7sWzF7djyi length=10
id | code
--------------------+------------
106666141551718400 | 7sWzEmdQLw
106666141778210816 | 7sWzF1xlNQ
106666141862096896 | 7sWzF7djyi
(3 rows)
+25
View File
@@ -0,0 +1,25 @@
PASS Base62Test.knownValues
PASS Base62Test.roundTripsAcrossTheRange
PASS Base62Test.rejectsGarbage
PASS Base62Test.sevenCharactersCoverAboutThreePointFiveTrillion
PASS CodeScramblerTest.noTwoIdsShareACodeAcrossTwoMillionConsecutiveIds
PASS CodeScramblerTest.everyCodeIsExactlySevenCharacters
PASS CodeScramblerTest.idsBeyondTheFortyBitDomainFallBackToPlainBase62
PASS CodeScramblerTest.consecutiveIdsDoNotGiveConsecutiveCodes
PASS RateLimitTest.burstOfFiveThenRejectedThenRefilled
PASS RateLimitTest.redirectsAreNeverRateLimited
PASS SnowflakeIdGeneratorTest.idsIncreaseWithinOneThread
PASS SnowflakeIdGeneratorTest.base62CodeIsLongerThanForASequenceId
PASS SnowflakeIdGeneratorTest.refusesToIssueIdsWhenTheClockGoesBackwards
PASS SnowflakeIdGeneratorTest.idsAreUniqueAcrossEightThreads
PASS SnowflakeIdGeneratorTest.nodeIdIsEncodedInTheId
PASS SnowflakeIdGeneratorTest.rejectsOutOfRangeNodeId
PASS SnowflakeProfileTest.snowflakeIdsGiveLongerCodes
PASS UrlShortenerApiTest.sameLongUrlGetsAnIndependentCodeEachTime
PASS UrlShortenerApiTest.customAliasWorksAndSecondUseIsAConflict
PASS UrlShortenerApiTest.unknownCodeIs404AndTheMissIsNegativelyCached
PASS UrlShortenerApiTest.createsAShortLinkAndRedirectsWith302
PASS UrlShortenerApiTest.countsClicks
PASS UrlShortenerApiTest.rejectsBadInput
PASS UrlShortenerApiTest.expiredLinkIs410
24 tests, 0 failures