Design: URL shortener

System Design · lesson 13 of 32 · 5 min read

Walk a full small design end to end: key generation, storage, redirects, and the read path.

Open this lesson in the learning hub

Key points

  • Requirements: POST /links returns a short code, GET /{code} redirects. Roughly 100 reads per write, so optimise the read.
  • Generate the code from a unique 64-bit id encoded in base62. Seven characters cover 3.5 trillion links, and no collision check is needed.
  • Hashing the URL instead means collisions, and collisions mean a read-check-retry loop on the write path. Counters are simpler.
  • Storage is tiny: 500 bytes per row × 100M links is about 50 GB. One relational node handles it; the code is the primary key.
  • The read path is a key-value lookup, so cache aggressively - hot codes live in Redis and the database is the fallback.
  • Redirect with 302 if you want click analytics; 301 is cached by browsers and those hits never reach you again.

Example

// 62^7 = ~3.5 trillion codes. Encode a unique id - never a random guess.
private static final char[] B62 =
    "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray();

static String encode(long id) {
    StringBuilder sb = new StringBuilder(7);
    do {
        sb.append(B62[(int) (id % 62)]);
        id /= 62;
    } while (id > 0);
    return sb.reverse().toString();
}

@RestController
class LinkController {

    @PostMapping("/links")
    ResponseEntity<LinkResponse> create(@RequestBody @Valid CreateLink req) {
        long id = ids.next();                     // DB sequence, or a per-instance range
        String code = encode(id);
        links.save(new Link(code, req.url(), Instant.now()));
        return ResponseEntity.created(URI.create("/" + code))
                             .body(new LinkResponse(code));
    }

    @GetMapping("/{code}")
    ResponseEntity<Void> resolve(@PathVariable String code) {
        String url = cache.get(code, links::findUrlOrThrow);
        return ResponseEntity.status(HttpStatus.FOUND)   // 302: keeps analytics alive
                             .location(URI.create(url))
                             .build();
    }
}

Unique id plus base62 beats random-and-retry, and the whole read path is one cached lookup.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the System Design course, and every lesson in it is listed on the System Design contents page.