URL Shortener
Turn a long URL into a short code and back again. The whole problem is really one question: how do you generate a code that's short, doesn't collide, and doesn't require a lookup just to create it.
Requirements
Functional
- A caller submits a long URL and gets back a short code.
- Visiting the short code redirects to the original long URL.
- The same long URL submitted twice may produce two different short codes (no dedup requirement) - but a given short code must always resolve to the same long URL.
- A short code should be reasonably short (six or seven characters is the usual target).
Non-functional
- Generating a code must not require scanning existing codes for a collision under normal operation; a collision, if it happens, must be detected and retried.
- Swapping the encoding scheme (base62 counter vs. a hash-based approach) must not require
changing
UrlShortenerServiceorUrlRepository.
Design
UrlShortenerService owns the flow - generate a code, check it's free, store the mapping -
but never owns the encoding itself. A CodeGenerator interface produces candidate codes;
swapping a counter-based base62 generator for a hash-based one is a constructor argument,
not a rewrite.
- 1The caller only supplies the long URL - never a code, never an encoding choice.
- 2The service asks the generator for a candidate without knowing how it was produced.
- 3The service checks the repository for a collision before committing to the candidate.
- 4On the rare collision, the service asks again - the generator has no idea a retry happened.
- 5Once a free code is found, the mapping is persisted and returned to the caller.
Collision handling lives entirely in the service's retry loop: it asks the generator for a code, asks the repository if it's taken, and only loops if both come back with a conflict - the generator itself never needs to know about collisions.
Class diagram
implementsuses
Code
Design decisions
CodeGeneratoris a Strategy, not a static method on the service. A counter-based base62 generator and a hash-of-the-URL generator have completely different collision profiles (the counter one, correctly implemented, never collides; the hash-based one occasionally does) - keeping generation behind an interface means the service's retry logic works for either without caring which is plugged in.- Collision checking happens in the service, not inside the generator. A generator's only job is "produce a candidate code." Whether that candidate is already taken is a question about the repository's current state, which the generator has no business knowing about - so the retry loop belongs one layer up.
UrlRepositoryis an interface even though this page only shows an in-memory map. Swapping in a real key-value store later means implementing one interface, not hunting down every place the service touched aHashMapdirectly.- What's missing for a real system: a counter-based generator needs the counter itself
to be a distributed, monotonically increasing sequence (not a single in-process
long) once there's more than one service instance, and a real deployment would put a cache in front of the repository, since redirects vastly outnumber new-URL submissions.