LLD for URL Shortener: Designing TinyURL
How do you turn a long URL into a 7-character string? We explore Base62 encoding, Hash collisions, and database choices.
Abstract AlgorithmsAI-assisted content. This post may have been written or enhanced with AI tools. Please verify critical information independently.
TLDR
TLDR: A URL Shortener maps long URLs to short IDs. The core challenge is generating a globally unique, short, collision-free ID at scale. We use Base62 encoding on auto-incrementing database IDs for deterministic, collision-free short codes.
๐ The Locker Room Analogy
A locker room assigns locker numbers. The attendant doesn't randomly pick numbers โ she uses the next available sequential number. You get "Locker 5" โ you can find it instantly.
URL shorteners work the same way: assign a unique sequential integer ID, then encode it in a compact string format.
- Input:
https://www.google.com/search?q=system+design+interview - Auto-increment database ID:
125 - Base62 encode
125โcb - Short URL:
https://tiny.url/cb
๐ Short Codes, Redirects, and TTL: The Core Vocabulary
Before building, align on the four concepts everything else depends on.
Short Code โ the compact identifier that replaces a long URL. cb, Xk3, and my-blog are all short codes. They appear after the domain: https://tiny.url/cb. A good short code is short enough to type from memory and opaque enough not to leak information about neighbouring URLs.
Redirect โ when a user visits a short URL, the server looks up the original destination and sends back an HTTP redirect response (301 or 302). The browser follows it silently. The user never stays at tiny.url; they land on the real page within milliseconds.
TTL (Time to Live) โ links can expire. An event short link may be valid for 24 hours, a marketing campaign for 30 days, or a personal link forever. The expires_at column in the database enforces this. Expired codes return a 410 Gone or a branded expiry page.
Click Analytics โ every redirect is a chance to record who clicked, from where, and when. This is why most commercial shorteners (Bitly, t.co) default to 302 Temporary redirects โ the browser re-asks the server on each visit, so every click is counted server-side.
| Concept | What it means | Why it matters |
| Short Code | Compact alias for a long URL | Human-friendly, shareable |
| Redirect | HTTP 301/302 pointing to original URL | The core delivery mechanism |
| TTL | Expiry time on the short link | Campaign control and security |
| Click Analytics | Per-redirect tracking on the server | Business value of the service |
๐ข Why Base62 and Not MD5?
Approach A โ Hashing (MD5/SHA):
- Hash the long URL:
MD5("https://...") = 3b94d2a8... - Take first 7 chars:
3b94d2a. - Problem: Two different URLs might produce the same first 7 chars โ collision โ wrong redirect.
- Collision probability with 7 chars from MD5: ~1 in 1.4 billion โ sounds small, but with billions of URLs it happens.
Approach B โ Base62 on Auto-Increment ID (chosen):
- Database auto-increments: ID = 1, 2, 3, โฆ
- Encode:
toBase62(ID) - Guarantee: Every ID is unique โ every short code is unique. Zero collision probability.
Base62 alphabet: a-z (26) + A-Z (26) + 0-9 (10) = 62 characters.
$$62^7 \approx 3.5 \text{ trillion combinations with 7 characters}$$
More than enough for any URL shortening service.
CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
def encode(n: int) -> str:
if n == 0: return CHARS[0]
result = []
while n:
result.append(CHARS[n % 62])
n //= 62
return "".join(reversed(result))
def decode(s: str) -> int:
n = 0
for c in s:
n = n * 62 + CHARS.index(c)
return n
โ๏ธ OOP Contracts: Interfaces and Collaborations
The URL shortener's backbone is four focused Java interfaces. Each defines a contract โ an agreement about what a collaborator can do without revealing how it does it. UrlShortenerService holds a reference to each interface and receives them via constructor injection; it never references a concrete class directly.
interface UrlEncoder {
String encode(long id); // converts counter ID to Base62 short code
}
interface CodeStore {
String save(String longUrl); // stores mapping, returns short code
Optional<String> resolve(String code); // looks up long URL by short code
boolean exists(String code); // deduplication check
}
interface AnalyticsTracker {
void recordClick(String code, String referrer, Instant timestamp);
}
interface AliasValidator {
boolean isValid(String customAlias); // checks format, uniqueness, reserved words
}
Who implements each interface, who calls it, and what it abstracts:
| Interface | Implemented by | Called by | What it abstracts |
UrlEncoder | Base62Encoder | UrlShortenerService | The 62-char alphabet and modular arithmetic |
CodeStore | InMemoryStore, RedisStore, DistributedStore | UrlShortenerService | Persistence: hash map, Redis, or SQL |
AnalyticsTracker | NoOpTracker, DatabaseTracker | UrlShortenerService | Click recording โ optional and swappable |
AliasValidator | CustomAliasValidator | UrlShortenerService | Format rules, reserved-word checks, uniqueness |
Runtime collaboration โ how the components interact during a shorten() call:
flowchart LR
Client -->|"shorten(longUrl)"| SVC[UrlShortenerService]
SVC -->|"save(longUrl)"| Store[CodeStore (RedisStore / InMemoryStore)]
Store -->|shortCode| SVC
SVC -->|"encode(id)"| Encoder[UrlEncoder (Base62Encoder)]
Encoder -->|"cb"| SVC
SVC -->|shortCode| Client
Structural relationships โ ownership and implementation:
classDiagram
class UrlEncoder {
<>
+encode(id long) String
}
class CodeStore {
<>
+save(longUrl String) String
+resolve(code String) Optional~String~
+exists(code String) boolean
}
class AnalyticsTracker {
<>
+recordClick(code String, referrer String, timestamp Instant) void
}
class Base62Encoder {
-CHARS String
+encode(id long) String
}
class CustomAliasEncoder {
+encode(id long) String
}
class UrlShortenerService {
-encoder UrlEncoder
-store CodeStore
-tracker AnalyticsTracker
+shorten(longUrl String) String
+resolve(shortCode String) String
}
Base62Encoder ..|> UrlEncoder : implements
CustomAliasEncoder --|> Base62Encoder : extends
UrlShortenerService o-- CodeStore : uses (injected)
UrlShortenerService o-- UrlEncoder : uses (injected)
UrlShortenerService o-- AnalyticsTracker : uses (injected)
This class diagram shows the structural ownership relationships of the URL shortener, with UrlShortenerService at the centre depending on three injected interfaces (UrlEncoder, CodeStore, AnalyticsTracker) and CustomAliasEncoder extending Base62Encoder through inheritance. The interface arrows (...|>) versus the composition arrows (o--) deliberately separate implementation details from runtime dependencies, making it clear that the service never references a concrete class directly. Take away: the three o-- arrows from UrlShortenerService are the injection points โ swapping InMemoryStore for RedisStore requires changing only one constructor argument, not a single line of service logic.
๐งฑ OOP Pillars Applied to the URL Shortener
Each classical OOP pillar maps directly to a concrete design decision in this system.
Encapsulation โ Base62Encoder owns its alphabet
The 62-character alphabet (a-z A-Z 0-9) and the modular-arithmetic encoding loop are private implementation details inside Base62Encoder. Callers invoke encode(125) and receive "cb" โ they never see the CHARS constant or the while (id > 0) loop. Swapping the alphabet (say, to a URL-safe Base64 variant) requires editing exactly one class, nowhere else.
Abstraction โ CodeStore hides the backing technology
UrlShortenerService calls store.save(longUrl) and store.resolve(code). It has no idea whether the store is a HashMap in unit tests, a Redis HSET in staging, or a PostgreSQL INSERT in production. That decision lives at the injection site and never leaks into the service logic.
Inheritance โ CustomAliasEncoder extends Base62Encoder
When a user provides their own short code (e.g., my-blog), the system needs an encoder that accepts the user-supplied string directly instead of computing one. CustomAliasEncoder extends Base62Encoder and overrides encode() to return the validated alias โ reusing all of Base62Encoder's decoding support while replacing only the encoding direction. The same pattern applies to store variants: AtomicCounterStore implements CodeStore for single-node deployments; DistributedStore implements CodeStore for multi-node production with a Token Range Service.
Polymorphism โ UrlShortenerService holds a CodeStore reference
The service is written once against the CodeStore interface. Inject InMemoryStore for tests, RedisStore for production, or a DualWriteStore during a live migration โ the same shorten() and resolve() methods run identically regardless of which implementation sits behind the reference.
| Pillar | Applied via | Concrete location |
| Encapsulation | private CHARS constant hidden from callers | Base62Encoder |
| Abstraction | CodeStore interface hides persistence technology | UrlShortenerService โ CodeStore |
| Inheritance | CustomAliasEncoder extends Base62Encoder | Custom alias code path |
| Polymorphism | CodeStore reference accepts any implementation | UrlShortenerService constructor |
โ SOLID Principles in the URL Shortener Design
SOLID is not an abstract checklist โ each principle appears as a specific, verifiable choice here.
Single Responsibility (SRP) โ Each class has exactly one reason to change. Base62Encoder changes only if the encoding algorithm changes. A CodeStore implementation changes only if the persistence technology changes. AnalyticsTracker changes only if the analytics schema or sink changes. None of these concerns bleed into UrlShortenerService.
Open/Closed (OCP) โ Adding Base58 encoding? Create Base58Encoder implements UrlEncoder. Adding NanoID? Same: NanoIdEncoder implements UrlEncoder. In both cases UrlShortenerService is untouched โ the interface is the extension point, and the service is closed to modification.
Liskov Substitution (LSP) โ Any CodeStore implementation can substitute another without changing the observable behaviour for UrlShortenerService. InMemoryStore.save("https://example.com") returns a short code. RedisStore.save("https://example.com") also returns a short code. The service cannot tell the difference โ and it should not need to.
Interface Segregation (ISP) โ Four small, focused interfaces instead of one large UrlShortenerPort. A single fat interface would force every implementor to provide all six methods even when only one is needed. The four-interface design lets NoOpTracker implement just recordClick() trivially, without carrying dead weight from encoding or storage concerns.
Dependency Inversion (DIP) โ UrlShortenerService receives abstractions via constructor injection:
public class UrlShortenerService {
private final UrlEncoder encoder;
private final CodeStore store;
private final AnalyticsTracker tracker;
public UrlShortenerService(UrlEncoder encoder, CodeStore store, AnalyticsTracker tracker) {
this.encoder = encoder;
this.store = store;
this.tracker = tracker;
}
}
No new Base62Encoder(). No new RedisStore(). Concrete classes are wired by a Spring @Configuration class โ the service depends exclusively on interfaces.
๐ Interface Contracts: The Four Boundaries of the URL Shortener
Each interface is a stable boundary. Anything inside that boundary can change freely; anything outside can only call the methods listed here.
/**
* Converts a numeric counter ID into a compact short-code string.
* Implementations: Base62Encoder, CustomAliasEncoder, Base58Encoder
*/
interface UrlEncoder {
String encode(long id); // e.g. encode(125) โ "cb"
}
/**
* Persists and retrieves short-code โ long-URL mappings.
* Implementations: InMemoryStore (test), RedisStore (staging), DistributedStore (prod)
*/
interface CodeStore {
String save(String longUrl); // persists mapping, returns generated short code
Optional<String> resolve(String code); // returns long URL for code, or empty if not found
boolean exists(String code); // returns true if the code is already in use
}
/**
* Records analytics for every redirect event.
* Implementations: NoOpTracker (default), DatabaseTracker (analytics-on)
*/
interface AnalyticsTracker {
void recordClick(String code, String referrer, Instant timestamp);
}
/**
* Validates user-supplied custom aliases before storage.
* Implementations: CustomAliasValidator
*/
interface AliasValidator {
boolean isValid(String customAlias); // false if blank, reserved, non-alphanumeric, or taken
}
These four contracts are everything UrlShortenerService needs from the outside world. Any Spring @Bean that satisfies an interface can be wired in โ production configs, test configs, and migration configs all swap implementations without modifying the service itself.
๐ Enhanced Class Diagram: All Interfaces and Implementations
classDiagram
class UrlEncoder {
<>
+encode(id long) String
+decode(code String) long
}
class CodeStore {
<>
+save(longUrl String) String
+resolve(code String) Optional~String~
+exists(code String) boolean
}
class AnalyticsTracker {
<>
+recordClick(code String, referrer String, ts Instant) void
}
class AliasValidator {
<>
+isValid(customAlias String) boolean
}
class Base62Encoder {
-String CHARS
+encode(id long) String
+decode(code String) long
}
class CustomAliasEncoder {
+encode(id long) String
}
class InMemoryStore {
-Map~String,String~ store
+save(longUrl String) String
+resolve(code String) Optional~String~
+exists(code String) boolean
}
class RedisStore {
+save(longUrl String) String
+resolve(code String) Optional~String~
+exists(code String) boolean
}
class NoOpTracker {
+recordClick(code, referrer, ts) void
}
class DatabaseTracker {
+recordClick(code, referrer, ts) void
}
class CustomAliasValidator {
+isValid(customAlias String) boolean
}
class UrlShortenerService {
-UrlEncoder encoder
-CodeStore store
-AnalyticsTracker tracker
-AliasValidator validator
+shorten(longUrl String) String
+resolve(shortCode String) String
}
Base62Encoder ..|> UrlEncoder : implements
CustomAliasEncoder --|> Base62Encoder : extends
InMemoryStore ..|> CodeStore : implements
RedisStore ..|> CodeStore : implements
NoOpTracker ..|> AnalyticsTracker : implements
DatabaseTracker ..|> AnalyticsTracker : implements
CustomAliasValidator ..|> AliasValidator : implements
UrlShortenerService o-- UrlEncoder : uses (injected)
UrlShortenerService o-- CodeStore : uses (injected)
UrlShortenerService o-- AnalyticsTracker : uses (injected)
UrlShortenerService o-- AliasValidator : uses (injected)
This enhanced class diagram expands the earlier view to include all four interface hierarchies โ UrlEncoder, CodeStore, AnalyticsTracker, and AliasValidator โ along with their concrete implementations, showing the complete set of injection points and their swap-in alternatives. The pattern of multiple implements arrows per interface (e.g., InMemoryStore and RedisStore both implementing CodeStore) illustrates that switching storage backends from in-memory testing to production Redis is a one-line constructor change. Take away: this diagram is the dependency map for the URL shortener โ every arrow from UrlShortenerService is a seam where a real implementation can be replaced with a stub in tests.
๐ URL Shortening in Motion: The End-to-End Request Flow
There are two fundamentally different operations in a URL shortener: creating a short link and resolving one. They travel very different code paths.
Creation flow โ the write path. Happens once per link, so throughput requirements are moderate:
sequenceDiagram
participant Client
participant API
participant DB
Client->>API: POST /api/shorten {longUrl}
API->>DB: INSERT long_url returns auto-increment ID = 125
DB-->>API: ID = 125
API->>API: Base62(125) = "cb"
API-->>Client: {shortUrl: "https://tiny.url/cb"}
Resolution flow โ the read path. Happens millions of times per link, so every millisecond matters. This is the Redis cache-first path wired via @Cacheable in the Spring Boot section below โ cache hits serve from Redis in under 1 ms; cold misses fall through to the database and write back to the cache automatically.
The asymmetry is intentional: writes happen once; reads happen constantly. A Redis cache on the resolution path keeps p99 redirect latency under 5 ms for hot links, while the database only sees cold-cache misses. Design your system around the read-heavy reality from the start.
๐ URL Resolve (Redirect) Sequence
sequenceDiagram
participant B as Browser
participant API as UrlShortenerService
participant CS as CodeStore
participant AT as AnalyticsTracker
B->>API: GET /cb (short code)
API->>CS: resolve(cb)
CS-->>API: Optional.of(https://example.com)
API->>AT: recordClick("cb", referrer, now)
AT-->>API: void
API-->>B: 301 Redirect https://example.com
Note over B,API: Cache miss path CS queries Redis then DB
Note over B,API: Cache hit path CS returns from Redis in under 1ms
This resolution sequence diagram traces the hot path of the URL shortener โ the redirect read that happens millions of times per day for popular short codes โ showing how the CodeStore abstraction enables a two-tier lookup (Redis cache โ database) transparent to the caller. The notes at the bottom highlight the two execution paths: a cache hit returns in under 1ms from Redis, while a cache miss falls through to the database and writes back to the cache for subsequent requests. Take away: the CodeStore interface is what makes this caching strategy testable in isolation โ the service never knows whether it is talking to Redis or a hash map.
๐ง Deep Dive: Scaling the ID Generator
Auto-increment works with a single DB shard. At scale:
| Approach | How | Tradeoff |
| DB Auto-Increment | Sequential IDs, single write master | Bottleneck at DB master |
| Token Range Service | Each app server claims a range (1โ1000, 1001โ2000, โฆ) | Low contention; predictable ranges |
| Twitter Snowflake | 41-bit timestamp + 10-bit machine ID + 12-bit sequence | 64-bit globally unique, time-sortable, no coordination needed |
| UUID | 128-bit random, no coordination | Too long for a short URL; not sortable |
Token Range Service is the sweet spot for most URL shorteners:
- App server asks "Range Service" for the next batch of 1000 IDs.
- App server assigns IDs locally from its batch.
- Range Service only coordinates batch handoffs, not individual URLs.
โ๏ธ Trade-offs & Failure Modes: Custom Aliases and Collision Handling
Users sometimes want custom codes: tiny.url/my-blog.
- Check if
my-blogexists in the DB. - If not: insert {short_code: "my-blog", long_url: "..."}.
- If yes: return "Code already taken" error.
Custom aliases bypass the Base62 auto-increment path and are stored directly.
๐ Short URL Lifecycle States
stateDiagram-v2
[*] --> ACTIVE : shorten(longUrl) code generated and stored
ACTIVE --> ACTIVE : resolve(code) redirect served
ACTIVE --> EXPIRED : TTL reached (if set)
ACTIVE --> DELETED : owner deletes alias
EXPIRED --> [*]
DELETED --> [*]
note right of ACTIVE : Every GET increments click count via AnalyticsTracker
This state diagram captures the full lifecycle of a short URL from creation through its possible end states, showing that ACTIVE is the only state that generates redirects and analytics events. The ACTIVE โ EXPIRED and ACTIVE โ DELETED transitions model two distinct termination reasons โ system-enforced TTL expiry and owner-initiated deletion โ both of which lead to terminal states with no path back. Take away: modeling short URL states explicitly prevents bugs like serving expired codes as valid redirects or counting clicks against deleted aliases.
๐ Real-World Applications of URL Shorteners
URL shorteners are not just a convenience tool โ they underpin several critical web workflows across the industry.
Social media sharing: Twitter's t.co wraps every URL posted in a tweet. This lets Twitter track engagement, enforce character limits, and scan destination links for malware before users click. Every tweet ever published with a URL goes through this shortening layer.
QR code campaigns: A QR code printed on a poster or product package cannot be edited after printing. But if it encodes a short URL like tiny.url/event24, you can update the destination in the database without reprinting anything. This makes short URLs essential for physical-world marketing where the campaign destination may change after launch.
Email marketing: Campaign links typically carry UTM parameters โ ?utm_source=newsletter&utm_campaign=spring-launch&utm_medium=email โ which inflate URL length significantly and look ugly in email copy. Short URLs keep the email readable while preserving full analytics on the server side.
Internal developer tooling: Engineering teams build private shorteners (often called "go links") for long dashboard URLs, runbook links, JIRA queries, and Grafana panels. go/deploy-prod beats any bookmark. Google pioneered this internally and the pattern is widespread at large tech companies.
Common thread: all of these use cases decouple the publicly shared link from the destination, so destinations can be changed, tracked, A/B tested, and secured independently of the URL already in circulation.
๐งช Hands-On: Extend and Stress-Test Your URL Shortener
Put the concepts to work with these three targeted exercises.
Exercise 1 โ Implement and Verify Base62 Encode/Decode
Take the Python snippet from the Base62 section and test these boundary conditions:
encode(0)should return"a"(the first character in the alphabet).encode(61)should return"9"(the last character).encode(62)should return"ba"(first two-character code).decode(encode(12345))should round-trip back to12345.
If any assertion fails, check whether your alphabet ordering matches a-z A-Z 0-9.
Exercise 2 โ Design the "Update Destination" Feature
A user wants to change where tiny.url/cb points without changing the short code. Write:
- The SQL
UPDATEstatement that changes thelong_url. - The Redis
DEL cbcommand that invalidates the stale cache entry.
Why must the cache invalidation happen before or at the same time as the DB update? What failure scenario occurs if you invalidate cache after the DB update with a delay?
Exercise 3 โ 301 vs. 302 Trade-off Audit
You ran a campaign using HTTP 301 for redirect performance. Mid-campaign, the landing page URL changed. Document the exact failure: which users are affected, what they see, and why the server cannot help them. Then explain how switching to HTTP 302 would have prevented it โ and what the bandwidth cost of 302 is at 10 million redirects per day.
๐งญ Decision Guide: Choosing the Right URL Shortener Architecture
|---|---| | Collision-free short codes | Base62 on auto-increment ID โ zero collision by construction | | Analytics tracking needed | HTTP 302 (temporary redirect) โ browser never caches, every click hits your server | | No analytics needed | HTTP 301 (permanent redirect) โ browser caches, reduces server load | | Scale beyond single DB | Token Range Service (simplest) or Snowflake IDs (globally unique, time-sortable) | | Popular codes hit DB | Redis cache with 24-hour TTL on the read path | | Users need custom aliases | Separate code path bypassing auto-increment; check existence before insert |
Start with a single DB + auto-increment. Add Redis when cache miss rate rises above 5 %. Move to Token Range Service when the write master becomes the bottleneck. Add custom alias support only if users request it.
๐ ๏ธ Spring Boot and Redis: Production URL Shortener with Cache-Through Redirect
Spring Boot wires the URL shortener service, REST controller, and database repository into a running application with embedded Tomcat and auto-configured Jackson serialization. Spring Data Redis (via RedisTemplate or @Cacheable) provides the read-through Redis cache layer that sits in front of the database โ ensuring redirects are served in under 1ms from cache on cache hits.
How they solve the problem in this post: The UrlShortenerService implements the Base62-encode-on-auto-increment-ID strategy from this post. Spring Data Redis caches the shortCode โ longUrl mapping in Redis. On a redirect, the service checks Redis first (O(1), sub-millisecond); on a cache miss it queries the DB and writes back to Redis โ the classic cache-through pattern.
// โโโ URL entity (JPA, stored in PostgreSQL / MySQL) โโโโโโโโโโโโโโโโโโโโโโโโโโโ
import jakarta.persistence.*;
@Entity
@Table(name = "urls")
public class UrlMapping {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private long id; // auto-increment โ we Base62-encode this
@Column(nullable = false, length = 2048)
private String longUrl;
private java.time.Instant expiresAt; // null = never expires
// getters / setters (or Lombok @Data)
}
// โโโ Base62 encoder: the core encoding function โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
public class Base62 {
private static final String CHARS =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
public static String encode(long id) {
if (id == 0) return "0";
StringBuilder sb = new StringBuilder();
while (id > 0) {
sb.append(CHARS.charAt((int)(id % 62)));
id /= 62;
}
return sb.reverse().toString(); // e.g. 125 โ "cb"
}
}
// โโโ Service: Base62 ID generation + Redis cache-through โโโโโโโโโโโโโโโโโโโโโ
import org.springframework.cache.annotation.*;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.*;
interface UrlMappingRepository extends JpaRepository<UrlMapping, Long> {
java.util.Optional<UrlMapping> findByLongUrl(String longUrl);
}
@Service
public class UrlShortenerService {
private final UrlMappingRepository repo;
public UrlShortenerService(UrlMappingRepository repo) {
this.repo = repo;
}
/** Shorten: persist long URL, return Base62-encoded ID as short code */
public String shorten(String longUrl) {
return repo.findByLongUrl(longUrl)
.map(existing -> Base62.encode(existing.getId()))
.orElseGet(() -> {
UrlMapping m = new UrlMapping();
m.setLongUrl(longUrl);
UrlMapping saved = repo.save(m); // auto-increment ID assigned
return Base62.encode(saved.getId()); // e.g. 12345 โ "dnh"
});
}
/** Resolve: decode short code โ DB id โ long URL (Redis caches this) */
@Cacheable(value = "urls", key = "#shortCode") // cache key: "urls::dnh"
public String resolve(String shortCode) {
long id = decodeBase62(shortCode);
return repo.findById(id)
.map(UrlMapping::getLongUrl)
.orElseThrow(() -> new java.util.NoSuchElementException("Not found: " + shortCode));
}
private long decodeBase62(String code) {
long id = 0;
for (char c : code.toCharArray()) {
id = id * 62 + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".indexOf(c);
}
return id;
}
}
// โโโ REST controller โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
@RestController
public class UrlShortenerController {
private final UrlShortenerService svc;
public UrlShortenerController(UrlShortenerService svc) { this.svc = svc; }
/** POST /shorten body: { "url": "https://..." } */
@PostMapping("/shorten")
public ResponseEntity<Map<String, String>> shorten(@RequestBody Map<String, String> body) {
String shortCode = svc.shorten(body.get("url"));
return ResponseEntity.ok(Map.of(
"shortCode", shortCode,
"shortUrl", "https://tiny.url/" + shortCode));
}
/** GET /{shortCode} โ 302 redirect to long URL */
@GetMapping("/{shortCode}")
public ResponseEntity<Void> redirect(@PathVariable String shortCode) {
String longUrl = svc.resolve(shortCode); // Redis hit: <1ms; DB miss: ~5ms
return ResponseEntity.status(HttpStatus.FOUND)
.location(java.net.URI.create(longUrl)).build();
}
}
Configure Redis caching in application.yml:
spring:
cache:
type: redis
data:
redis:
host: localhost
port: 6379
cache:
redis:
time-to-live: 86400000 # 24h TTL โ expired short codes auto-evict from cache
Cache-hit redirects serve from Redis in under 1ms. Cache-miss redirects hit PostgreSQL (~5ms) and write back to Redis automatically via @Cacheable. The time-to-live: 24h ensures Redis memory is bounded โ only hot short codes stay in cache.
For a full deep-dive on Spring Boot cache-through patterns and Redis cluster configuration, a dedicated follow-up post is planned.
๐ Key Lessons from Building a URL Shortener
- Encoding beats hashing for uniqueness guarantees. Base62 on a sequential ID is collision-free by construction โ every ID is unique, so every short code is unique. MD5 prefix truncation is probabilistic; at billions of URLs it collides.
- Redirect semantics affect analytics permanently. Once a
301is cached by a browser, there is no server-side way to correct it. Decide your redirect strategy at schema design time, not after launch. - The read path dominates. A URL shortener is a read-heavy system by a wide margin โ thousands of redirects for every single create. Cache aggressively on the read path; don't over-engineer the write path.
- ID generation is the scalability bottleneck at high write volume. Move from single-DB auto-increment to a Token Range Service or Snowflake IDs before write contention becomes a problem.
- Custom aliases need their own code path. Keep them isolated from the auto-increment sequence to avoid polluting the ID space and making short codes predictable from the alias.
๐ TLDR: Summary & Key Takeaways
- Base62 on auto-increment ID is collision-free and generates compact 7-char codes for up to 3.5 trillion URLs.
- 301 saves bandwidth but loses analytics; 302 preserves click tracking โ use 302 for production analytics.
- Redis cache on the read path eliminates DB hits for popular short codes.
- Token Range Service or Snowflake IDs scale the ID generator beyond a single DB.
๐ Related Posts
Explore more LLD and system design patterns that complement the URL shortener:
- LLD for Parking Lot System โ sequential ID assignment and slot management patterns.
- LLD for Elevator System โ queue-based request handling and state machine design.
- LLD for Movie Booking System โ concurrency control and seat reservation at scale.
- LLD for LRU Cache: Designing a High-Performance Cache โ how the Redis cache layer behind your redirect path works internally.
Test Your Knowledge
Ready to test what you just learned?
AI will generate 4 questions based on this article's content.
Tags

Written by
Abstract Algorithms
@abstractalgorithms
More Posts
RAG vs Fine-Tuning: When to Use Each (and When to Combine Them)
TLDR: RAG gives LLMs access to current knowledge at inference time; fine-tuning changes how they reason and write. Use RAG when your data changes. Use fine-tuning when you need consistent style, tone, or domain reasoning. Use both for production assi...
Fine-Tuning LLMs with LoRA and QLoRA: A Practical Deep-Dive
TLDR: LoRA freezes the base model and trains two tiny matrices per layer โ 0.1 % of parameters, 70 % less GPU memory, near-identical quality. QLoRA adds 4-bit NF4 quantization of the frozen base, enabling 70B fine-tuning on 2ร A100 80 GB instead of 8...
Build vs Buy: Deploying Your Own LLM vs Using ChatGPT, Gemini, and Claude APIs
TLDR: Use the API until you hit $10K/month or a hard data privacy requirement. Then add a semantic cache. Then evaluate hybrid routing. Self-hosting full model serving is only cost-effective at > 50M tokens/day with a dedicated MLOps team. The build ...
Watermarking and Late Data Handling in Spark Structured Streaming
TLDR: A watermark tells Spark Structured Streaming: "I will accept events up to N minutes late, and then I am done waiting." Spark tracks the maximum event time seen per partition, takes the global minimum across all partitions, subtracts the thresho...
