Skip to content
SparkTime, home
System Design From Zero to Interview-Ready
Lessons
1 · System Design Foundations
2 · APIs and Communication
3 · Capacity Estimation
4 · Core Building Blocks
5 · Databases
6 · Distributed Systems
7 · Reliability and Operations
8 · Architecture and Security
9 · Interview Method and Foundation Case Studies
10 · Real-World Case Studies
11 · Large-Scale Case Studies
12 · Interview Readiness
Lesson 30 of 45

Lesson 30 · Interview Method and Foundation Case Studies

Design a URL Shortener (and Pastebin): A Complete Walkthrough

The classic first system design problem, worked end to end with the course's method: requirements, estimates, APIs, data model, a simple baseline, short-code generation and collisions, 301 versus 302 redirects, caching a read-heavy path, expiry, analytics off the critical path, sharding, failures, abuse prevention, and the Pastebin variant with object storage.

On this page

Key takeaways

  • A URL shortener is a small, extremely read-heavy key-value lookup; the design effort goes into the redirect path, not the write path.
  • Random 7-character base-62 codes give trillions of possibilities, unguessable links and rare collisions handled by a unique constraint; counter-based codes avoid collisions but are enumerable.
  • A cache in front of the database, with write-through on creation, serves most redirects in milliseconds and keeps new links working immediately.
  • Analytics belongs on an asynchronous click stream, never in the redirect's critical path; the redirect status code (302 versus 301) decides whether you can count and change links.
  • Abuse (phishing and malware links) is the real operational risk, so creation needs rate limits, URL checks and a takedown path.

"Design a URL shortener" is often the first system design question people meet, and it is easy to underestimate. The functional core fits in one sentence: turn a long URL into a short code, and redirect anyone who visits the code. Yet a good answer touches estimation, key generation, caching, consistency, failure handling and abuse, and the same reasoning applies to any read-heavy lookup service.

This is the first full case study of the course. Every case study from here on uses the same fourteen sections, which follow the seven phases from article 29. Read it once straight through, then try the problem yourself with a 45-minute timer before rereading.

What you will learn

#
  • How to scope a deceptively simple problem.
  • How to estimate a read-heavy system and what the numbers imply.
  • Three ways to generate short codes, and how to handle collisions.
  • Why the choice between 301 and 302 redirects matters.
  • How to scale the redirect path with caching, replicas and, eventually, sharding.
  • How to add analytics and expiry without slowing redirects.
  • How the design fails and how it is secured.
  • How the same design becomes Pastebin.

1. The problem

#

Users submit a long URL such as https://www.example.com/articles/2026/10/how-to-plan-a-budget-trip-to-kerala?ref=newsletter and receive a short link such as https://pix.ly/a8Kq2Zx. Anyone who opens the short link is redirected to the original URL. The service is public: anonymous users and signed-in users can create links, and links are shared widely on social media and in messages.

2. Clarify the requirements

#

Questions worth asking (and assumed answers):

Question

Assumed answer

Design impact

How many new links, and how many redirects?

100 million new links a month; about 100 redirects per link on average

Read-heavy; sizes cache and storage

Custom aliases (pix.ly/my-event)?

Yes, optional

Uniqueness check on user-chosen keys

Expiry?

Optional expiry date; default: never

Expiry checks and cleanup

Can links be edited or deleted?

Owners can delete; no editing of the target

Affects cache invalidation and redirect codes

Analytics?

Click counts per link, available to owners within minutes

Asynchronous click pipeline

Accounts?

Optional; anonymous creation allowed with stricter limits

Abuse controls

Global users?

Yes

CDN or regional serving for redirects

Functional requirements

  1. Create a short link for a long URL, optionally with a custom alias and an expiry time.
  2. Redirect a short link to its long URL.
  3. Let owners delete their links.
  4. Show owners click counts per link (minutes of delay acceptable).

Out of scope: link editing, detailed analytics dashboards, QR codes, team workspaces.

Non-functional requirements

Quality

Target

Redirect latency

p99 under 50 ms at the server (excluding the network to the user)

Availability

Redirects 99.99%: a broken short link in a printed poster cannot be fixed; creation 99.9%

Durability

A created link is never lost

Correctness

A code never points to the wrong URL; codes are never reused

Consistency

A new link works immediately for its creator; click counts may lag by minutes

Security

Resist abuse (phishing, malware, spam); codes not guessable in sequence

The requirement that drives the design: redirects are an extremely read-heavy, latency-sensitive key lookup that must be very highly available.

3. Assumptions and scale estimation

#

Writes (new links)

Code
100 million per month / (30 x 86,400 s) = about 40 per second on averagePeak factor 3: about 120 per second

Reads (redirects)

Code
100 redirects per link -> 10 billion redirects per month10 billion / 2.6 million s = about 3,900 per second on averagePeak factor 3: about 12,000 per secondRead:write ratio: 100 to 1

Storage

Code
Per link: code (7 bytes), long URL (average about 200 bytes, maximum 2 KB),owner, timestamps, expiry, flags: about 500 bytes with overheadsPer year: 1.2 billion links x 500 bytes = 600 GBFive years: 6 billion links, about 3 TB (before replicas)

Cache

Access is skewed: recent and viral links get most clicks. Assume the hottest 20 million links (about the last few weeks of popular links) serve most redirects: 20 million x 500 bytes = 10 GB. That fits comfortably in memory on a small replicated cache cluster.

Bandwidth

A redirect response is tiny (headers and a short body, under 1 KB): 12,000 per second x 1 KB = 12 MB per second, about 0.1 Gbps. Bandwidth is not a concern.

Conclusions: writes are modest (one database primary is plenty); storage grows to a few terabytes over years (one database can hold it, partitioning is a later option); reads are the hard part, at 12,000 per second with a tight latency target, which calls for caching.

4. API design

#
Code
POST /v1/linksIdempotency-Key: 3c1f...                (so a retried create does not make two links){ "long_url": "https://www.example.com/...", "custom_alias": "kerala-trip", "expires_at": "2027-01-01T00:00:00Z" }201 CreatedLocation: /v1/links/kerala-trip{ "code": "kerala-trip", "short_url": "https://pix.ly/kerala-trip", "expires_at": "2027-01-01T00:00:00Z" }Errors: 400 malformed URL; 409 alias already taken; 422 URL not allowed (blocked domain); 429 rate limited
Code
GET /{code}302 FoundLocation: https://www.example.com/...Errors: 404 unknown code; 410 Gone for an expired or deleted link
Code
DELETE /v1/links/{code}          -> 204 (owner only)GET    /v1/links/{code}/stats    -> 200 {"clicks": 18234, "as_of": "..."} (owner only)

301 or 302?

#

HTTP defines 301 Moved Permanently and 308 Permanent Redirect, which clients and caches may remember, and 302 Found and 307 Temporary Redirect, which they should not treat as permanent (RFC 9110).

301 (or 308) permanent

302 (or 307) temporary

Browser behaviour

May cache the redirect and skip our server next time

Asks our server each time

Load on our servers

Lower

Higher

Click counting

Repeat visits from the same browser are invisible

Every visit is seen

Deleting or expiring a link

Cached redirects may keep working

Takes effect immediately

Because we need click counts and the ability to delete and expire links, choose 302. (A shortener without analytics or expiry could prefer 301 to reduce load.) This is a good example of a small decision with consequences worth stating in an interview.

5. Data model

#

The only hot access pattern is "find the long URL by code": a point lookup by key.

SQL
CREATE TABLE links (    code        VARCHAR(32) PRIMARY KEY,       -- random code or custom alias    long_url    VARCHAR(2048) NOT NULL,    owner_id    BIGINT,                         -- null for anonymous links    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),    expires_at  TIMESTAMPTZ,    deleted_at  TIMESTAMPTZ,    is_custom   BOOLEAN NOT NULL DEFAULT false);CREATE INDEX links_by_owner ON links (owner_id, created_at DESC) WHERE owner_id IS NOT NULL;CREATE INDEX links_expiring ON links (expires_at) WHERE expires_at IS NOT NULL AND deleted_at IS NULL;CREATE TABLE link_click_counts (    code        VARCHAR(32) PRIMARY KEY REFERENCES links(code),    clicks      BIGINT NOT NULL DEFAULT 0,    updated_at  TIMESTAMPTZ NOT NULL);
  • The primary key on code makes redirects a single index lookup and enforces uniqueness for both random codes and custom aliases.
  • links_by_owner serves "my links" pages; partial indexes keep the expiry job cheap.
  • Click counts are separate, updated in batches by the analytics pipeline, so redirects never write to links.
  • Partition key if we ever shard: code. Every redirect includes it, and random codes spread evenly.

Relational or key-value? The access pattern is pure key-value, so a key-value store (for example DynamoDB or Cassandra) would fit well. A relational database also fits: a few terabytes over five years, about 120 writes per second at peak, and point reads that are mostly served by the cache. Either is defensible. Choose the one the team operates confidently, and say that the access pattern makes a later migration to a key-value store straightforward.

6. Baseline architecture

#
Code
               create (POST)                            redirect (GET /code)Client ---------------------------> Load balancer <------------------------- Browser                                          |                                    App servers (stateless)                                          |                                     PostgreSQL (primary)

Create flow: validate the URL; generate a code (section 7); insert the row (the primary key rejects duplicates); return 201.

Redirect flow: look up code; if it is missing, deleted or expired, return 404 or 410; otherwise return 302 with the Location.

This works for a small number of users. At 12,000 redirects per second, something breaks first.

7. What breaks first?

#
  1. Database reads. 12,000 point lookups per second at peak, with a 50 ms p99 target, all hitting one primary. Even if the database could keep up, it leaves no headroom, and any slowdown shows up directly in redirect latency.
  2. Code generation. How do we create unique, short, unguessable codes, possibly on many app servers at once?
  3. Hot links. A viral link can receive a large share of all traffic.
  4. Analytics. Counting clicks naively (an UPDATE per redirect) would add 12,000 writes per second to the database.

Generating short codes

#

How long must codes be? Using the 62 characters 0-9, a-z and A-Z, 6 characters give 62^6 = about 57 billion codes and 7 characters give 62^7 = about 3.5 trillion. Five years of links (6 billion) fits in either, but random codes collide more often as the space fills, so 7 characters gives comfortable room.

Three approaches:

Approach

How

Pros

Cons

Hash the URL

Hash the long URL (for example SHA-256), base-62 encode, take the first 7 characters

Same URL can map to the same code (deduplication)

Collisions between different URLs must still be handled; two users shortening the same URL share a code and its analytics, which is often unwanted

Counter + base 62

A unique number per link (from a sequence, or from ranges of numbers allocated to each server) encoded in base 62

No collisions; short codes early on

Sequential codes are enumerable: anyone can walk ...a8Kq2Zx, a8Kq2Zy and discover links; the counter must be coordinated across servers

Random code + uniqueness check

Generate 7 random base-62 characters; insert; on a primary-key conflict, generate another

Unguessable; no coordination; simple

Small chance of collision, handled by a retry

This Java program shows base-62 encoding and decoding of counter values, the size of the 7-character space, the collision probability for a random code once 6 billion links exist, and some random codes (with a fixed seed so the output is repeatable; production code would use a secure random source).

Java
// File: ShortCodes.javaimport java.util.Locale;import java.util.Random;public class ShortCodes {    static final String ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";    static final int BASE = ALPHABET.length();   // 62    // Counter-based codes: encode a unique number in base 62.    static String encode(long n) {        if (n == 0) {            return "0";        }        StringBuilder sb = new StringBuilder();        while (n > 0) {            sb.append(ALPHABET.charAt((int) (n % BASE)));            n /= BASE;        }        return sb.reverse().toString();    }    static long decode(String code) {        long n = 0;        for (char c : code.toCharArray()) {            n = n * BASE + ALPHABET.indexOf(c);        }        return n;    }    // Random codes: pick each character at random (in production, from a secure random source).    static String randomCode(Random random, int length) {        StringBuilder sb = new StringBuilder();        for (int i = 0; i < length; i++) {            sb.append(ALPHABET.charAt(random.nextInt(BASE)));        }        return sb.toString();    }    public static void main(String[] args) {        for (long n : new long[] {1, 61, 62, 125_000_000_000L}) {            String code = encode(n);            System.out.println(n + " -> " + code + " -> " + decode(code));        }        long space7 = (long) Math.pow(BASE, 7);        System.out.printf(Locale.US, "7-character space: %,d codes%n", space7);        System.out.println("largest 7-character code: " + encode(space7 - 1));        long used = 6_000_000_000L;               // 5 years of links in the worked example        System.out.printf(Locale.US, "chance a new random code collides after %,d links: %.2f%%%n",                used, 100.0 * used / space7);        Random random = new Random(7);            // fixed seed only so this output is repeatable        System.out.println("random codes: " + randomCode(random, 7) + " " + randomCode(random, 7) + " " + randomCode(random, 7));    }}
Code
1 -> 1 -> 161 -> Z -> 6162 -> 10 -> 62125000000000 -> 2crtgcg -> 1250000000007-character space: 3,521,614,606,208 codeslargest 7-character code: ZZZZZZZchance a new random code collides after 6,000,000,000 links: 0.17%random codes: A0FQQ6c HISUmE3 jC7k3hA

Even after five years, a new random code collides with an existing one less than 0.2% of the time, so on average about one create in 600 needs a single retry, and two consecutive collisions are very rare. That retry is cheap and correct because the database's primary key is the referee: two servers that happen to generate the same code at the same instant cannot both insert it.

Decision: random 7-character codes, inserted with the primary key enforcing uniqueness, retrying up to a few times on conflict. Custom aliases use the same table and the same constraint; a conflict there returns 409 to the user instead of retrying. (If short, sequential-looking codes were a requirement, a counter with ranges allocated to each server, scrambled by a reversible permutation to avoid enumeration, would be the alternative.)

8. Scaling the design

#

Cache the redirect path

#

Add a distributed cache (for example Redis) in front of the database: cache-aside for reads, as described in Microsoft's Cache-Aside pattern (Cache-Aside pattern):

  • Redirect: look up link:{code} in the cache; on a hit, redirect; on a miss, read the database, store the result with a TTL of about a day (with jitter), redirect.
  • Create: after inserting the row, write it into the cache too, so the very first redirect (often the creator testing the link) is a hit and never depends on replica lag.
  • Delete: mark the row deleted, then delete the cache entry.
  • Unknown codes: cache "not found" for a short time (negative caching), so bots probing random codes do not reach the database.

With the 10 GB hot set cached and a hit ratio of, say, 95%, the database sees about 600 reads per second at peak instead of 12,000.

Read replicas and a small local cache

#
  • Cache misses go to read replicas, not the primary, which handles only about 120 writes per second.
  • Each app server keeps a tiny in-process cache (a few seconds' TTL) for the hottest codes, so a viral link does not overload one cache node (the hot-key problem from article 09).

Serving from the edge (optional)

#

Redirect responses for popular links can be cached at a CDN for a short time, which cuts latency for distant users and load on the origin. A CDN such as CloudFront routes each request to a nearby edge location (What is Amazon CloudFront?). The cost: clicks served at the edge are not seen by our servers, so counting must come from CDN logs, and deleting a link takes effect only after the edge TTL. Keep the edge TTL short (seconds to a minute) if used at all.

Analytics off the critical path

#

Never update a counter in the database on each redirect. Instead, after sending the redirect, the app server emits a click event (code, timestamp, coarse country and referrer) to a queue or stream, without waiting for an acknowledgement on the request path. Aggregation workers consume events, sum clicks per code per minute, and update link_click_counts in batches. If the analytics pipeline is down, redirects continue; events wait in the stream (article 11).

Expiry

#
  • On read: a redirect for an expired link returns 410 Gone, even if the row still exists. (The cached entry stores expires_at, so the cache checks it too.)
  • Cleanup: a background job uses the links_expiring index to find expired links in batches, marks them, and removes their cache entries.
  • Never reuse codes, even after expiry: an old printed link must not start pointing somewhere new.

Sharding, when needed

#

At 3 TB over five years and 120 writes per second, one primary with replicas is enough for a long time. If growth is ten times faster, partition links by hash of code across shards (article 16). Every redirect includes the code, so each lookup touches one shard, and random codes spread load evenly. "My links" queries by owner would then need a secondary index partitioned by owner, or the owner's links could be kept in a separate table keyed by owner.

The evolved architecture

#
Code
REQUEST PATH (redirect)Browser -> (optional CDN, short TTL) -> Load balancer -> App servers              App server: in-process hot cache -> Redis cache -> read replicas (on miss)              App server returns 302 immediatelyREQUEST PATH (create)Client -> Load balancer -> App servers -> URL safety checks -> Primary DB (unique code) -> write-through to RedisASYNC PATHApp servers --click events--> Stream --> Aggregation workers --> link_click_counts (batched)Scheduler --> Expiry job --> marks expired links, deletes cache entries

9. Failure scenarios and reliability

#

Failure

What happens

Response

One app server crashes

Load balancer removes it

Stateless servers; N+1 per zone

Cache node fails

Its keys miss; replicas take a burst of reads

Cache replicas with failover; in-process cache absorbs hot keys; request coalescing on misses

Whole cache cluster down

Up to 12,000 reads per second on replicas

Size replicas for a degraded mode, shed low-priority traffic (stats pages, creation) before redirects

Primary database down

Creates fail; redirects continue from cache and replicas

Synchronous standby and automatic failover; clients retry creation with the same idempotency key

Replica lag

A brand-new link could miss on a replica

Write-through to cache on creation; on a miss for a code younger than a minute, read the primary

Region down

Redirects fail in that region

DR replica in another region (warm standby, article 24); redirects are the priority to restore

Analytics stream down

Click events cannot be published

Buffer briefly in memory, then drop with a metric; redirects must never wait on analytics

Duplicate create (retry)

Could create two links

Idempotency key stored with the link

Concurrent identical random codes

Could collide

Primary-key constraint rejects one; it retries

The key reliability idea: the redirect path depends on as little as possible (app server, cache, replicas). Creation, analytics and expiry can degrade without breaking redirects.

10. Consistency

#
  • New links: strongly visible to the creator immediately, thanks to write-through to the cache (and primary reads for very young codes on a miss).
  • Deleted links: stop working after the cache entry is deleted; if edge caching is used, after its TTL as well. State this window.
  • Click counts: eventually consistent, minutes behind; acceptable per requirements.
  • Code uniqueness: strongly enforced by the database primary key, never by application checks.

11. Security

#

The real-world risk of a public shortener is abuse: attackers hide phishing and malware links behind a trusted-looking short domain.

  • Check URLs at creation against blocklists and reputation services, and re-scan popular links periodically; block known-bad domains (422).
  • Rate-limit creation per account, API key and IP; stricter for anonymous users; CAPTCHA or account requirement above a threshold (article 31).
  • Takedown path: abuse reports, a staff tool to disable links (with audit logs), and fast cache invalidation.
  • Unguessable codes (random, not sequential) so private links cannot be enumerated.
  • Validate input: only http and https URLs, maximum length, no javascript: or data: schemes.
  • Custom aliases: reserve system paths (/v1, /admin, /login), filter offensive or impersonating aliases.
  • Authorisation: only owners can delete links or see their stats (object-level checks, article 28).
  • Interstitial warning pages for links flagged as suspicious, rather than redirecting blindly.

12. Observability

#

Signal

Why

Redirect p50 and p99 latency, error rate

The main SLO

Cache hit ratio (and per-layer: in-process, Redis)

A drop predicts database overload

404 and 410 rates

Spikes may mean broken links being shared, or scanning by bots

Creation rate, rejected-URL rate, collision retries

Abuse detection and code-space health

Replica lag

Freshness of new links on misses

Click-event lag

Analytics delay against its minutes target

Synthetic checks

Create a link and follow it every minute from several regions

13. Trade-offs

#

Decision

Chosen

Alternative

Why

Redirect code

302

301

Need click counts, deletion and expiry

Code generation

Random 7 characters, unique constraint, retry

Counter + base 62

Unguessable, no coordination; collisions rare and cheap

Store

Relational with cache and replicas

Key-value store

Fits the scale and team; key-value is an easy later move

Cache writes

Write-through on create, cache-aside on read

Cache-aside only

New links must work immediately

Analytics

Asynchronous click stream, batched counts

Update per redirect

Keeps redirects fast and isolated

Edge caching

Optional, short TTL

None, or long TTL

Latency versus counting and deletion freshness

14. The final interview answer

#

"We're building a public URL shortener with about 100 million new links a month and a hundred times as many redirects, roughly 12,000 redirects a second at peak, with a p99 target of 50 ms and very high availability for redirects.

Links live in a table keyed by a 7-character random base-62 code; the primary key guarantees uniqueness, and the rare collision is retried. Custom aliases share the table. Creation takes an idempotency key, checks the URL against abuse lists, inserts, and writes the new link through to Redis so it works immediately.

Redirects go through stateless app servers that check a tiny in-process cache, then Redis, then read replicas, and return a 302, which we chose over 301 so we can count clicks and honour deletion and expiry. With a 95% hit rate the database sees about 600 reads a second. Clicks are published to a stream and aggregated asynchronously, so analytics can never slow or break redirects. Expiry is enforced on read and cleaned up by a background job.

The redirect path depends only on app servers, cache and replicas, so primary failover, analytics outages or creation problems don't stop redirects. If growth outpaces one primary, we shard by hash of the code. The biggest operational risk is abuse, handled by URL checks, rate limits and a takedown path."

The Pastebin variant

#

Pastebin lets users store text (code snippets, logs, notes) and share it by a short link. It is the URL shortener plus a blob:

Aspect

URL shortener

Pastebin

Stored per item

A URL (about 200 bytes)

Text from a few bytes to several megabytes

Where the content lives

In the database row

In object storage, with the database holding metadata and the object key (article 10)

Read response

A 302 redirect

The paste content (or a page that renders it)

Bandwidth

Tiny

Significant: average paste 10 KB x millions of reads

Extra features

Custom aliases

Syntax highlighting, burn-after-reading, private pastes, size limits

Changes to the design:

  • Create: the client sends the text (for large pastes, uploads it directly to object storage with a presigned URL, as AWS documents for S3 (presigned URLs)); the API stores metadata (code, object_key, size, content_type, expires_at, visibility) in the database.
  • Read: look up metadata (cached), then serve the content from object storage through a CDN, which absorbs the bandwidth. Public pastes can be cached at the edge with long TTLs because content is immutable (a paste is never edited; a new version is a new paste).
  • Expiry: object-storage lifecycle rules delete expired content; the metadata row returns 410.
  • Private pastes: short-lived signed URLs for content, plus authorisation on metadata.
  • Burn after reading: a conditional update marks the paste consumed on first read (UPDATE ... WHERE consumed = false), and only the request that changed the row gets the content: a nice small exercise in concurrency (article 14).
  • Abuse: pastes can contain leaked credentials or malicious content; add size limits, scanning, rate limits and a takedown process.

Exercises

#

Exercise 1

Redo the estimate

#

Redo the estimate for a shortener with 500 million new links a month, a read:write ratio of 20 to 1, a 10-year retention and 400 bytes per link. Compute peak writes and reads (peak factor 3), and total storage. Does one database still suffice?

Exercise 2: Counter-based codes

#

Your interviewer insists codes must be as short as possible early on, so they want counter-based codes. Design how 50 app servers get unique numbers without a single bottleneck, and how you stop people enumerating links.

#

A link is shared by a celebrity and receives 50,000 redirects per second for an hour, while the rest of the system receives its normal 12,000. Walk through what happens in this design and what protects it.

Exercise 4: Custom alias race

#

Two users submit the custom alias diwali-sale at the same moment. Walk through what happens and what each user sees. Then: what changes if links are stored in a key-value store without transactions?

Show solutionHide solution
Code
Writes: 500M / 2.6M s = about 190 per second; peak about 580 per secondReads: 20 x 500M = 10 billion per month = about 3,850 per second; peak about 11,500 per secondStorage: 500M x 12 months x 10 years = 60 billion links x 400 bytes = 24 TB (before replicas)

Reads are similar to the original example, so the caching design is unchanged. Writes (580 per second) are still fine for one well-provisioned primary, but storage (24 TB, about 72 TB with three copies) is beyond what you would want on one database node. Plan to shard by hash of code from the start (or use a key-value store that partitions automatically). Also check the code space: 60 billion links in 3.5 trillion 7-character codes is 1.7% used, so collisions rise to about 1 in 60 creates near the end; still acceptable with retries, or move to 8 characters.

Exercise 2

Show solutionHide solution

Range allocation: a small, highly available range service (backed by a database sequence or a consensus store) hands out blocks of numbers, for example 1,000,000 at a time. Each app server keeps its current block in memory and assigns numbers locally; when it runs low, it asks for the next block. The range service is called rarely (once per million links per server), so it is not a bottleneck, and a server crash only wastes the unused part of its block.

Preventing enumeration: encoding sequential numbers directly gives sequential-looking codes. Apply a reversible scrambling (a bijective permutation of the number space, such as a keyed format-preserving cipher or a multiply-and-modulo step with a secret constant) before base-62 encoding. Codes stay unique (the permutation is one-to-one) and short, but consecutive links no longer have adjacent codes. Also keep rate limits on lookups of unknown codes, so scanning is slow and visible.

Exercise 3

Show solutionHide solution

The hot code is one key. Without protection, all 50,000 requests per second would hit the single Redis node that owns it.

  • In-process caches on each app server serve it after the first lookup; with, say, 60 app servers and a 5-second TTL, Redis sees about 12 lookups per second for that key.
  • The load balancer spreads 50,000 + 12,000 = 62,000 requests per second across app servers; if that exceeds capacity, autoscaling adds servers (it takes minutes), and until then load shedding protects them, prioritising redirects over stats pages and creation.
  • Optional edge caching would serve most of the 50,000 at the CDN.
  • Click events for the viral link (50,000 per second) flow into the stream; aggregation workers batch them, so link_click_counts sees one update per minute for that code. If workers fall behind, analytics lags, but redirects are unaffected.
  • Abuse checks continue to apply: if the "celebrity" link turns out to be malicious, a takedown deletes the cache entries, and redirects return a warning page.

Exercise 4

Show solutionHide solution

In the relational design, both requests try INSERT ... (code = 'diwali-sale'). The primary key guarantees only one succeeds. The winner gets 201; the other gets a unique-violation error, which the API turns into 409 Conflict ("alias already taken, try another"). No double ownership is possible, whatever the timing or the number of app servers.

In a key-value store, use its conditional write: "put this item only if the key does not exist" (most key-value stores offer an equivalent of insert-if-absent, such as a condition on the key's existence). The semantics are the same: one write succeeds, the other is rejected and returns 409. What you must not do is "read to check, then write", which reintroduces the check-then-act race from article 14.

Where to go next

#

Sources & further reading

  1. rfc-editor.org
  2. learn.microsoft.com
  3. docs.aws.amazon.com
  4. docs.aws.amazon.com
  • Guide · Technology

    Start Here: Your Roadmap from Zero to Job-Ready DSA

  • Guide · Technology

    Heaps and Priority Queues

  • Guide · Career

    A LinkedIn Profile That Gets Recruiter Messages