System Design · Beginner Walkthrough

Design Pastebin: A Beginner-Friendly System Design Walkthrough

The ideal first system design problem, solved the way you should present it in a 45-minute interview: every decision explained, every number calculated, every trade-off named.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 16 min read
Code pasted onto a clipboard turning into a short link key that flows into cloud storage with an expiry hourglass
TL;DR
  • Pastebin = write text, get a short key, read by key. It is read-heavy (assume ~10:1) and the content is immutable, which makes caching easy.
  • Put paste content in object storage (S3) and metadata in a database. Keeps the database small and storage cheap.
  • Keys: 7 base62 characters give ~3.5 trillion options. Generate randomly with a uniqueness check, or pre-generate them with a key generation service.
  • Expiry is two-layered: check on read (correctness) plus a background cleanup or lifecycle rule (storage reclamation).
  • Reads go CDN → cache → object storage. View counts are collected asynchronously, never as a database write per view.

If you are new to system design interviews, Pastebin is the problem to start with. It has every core ingredient (requirements, estimation, an API, storage choice, key generation, caching, cleanup) but no single part is so hard that it hides the method. Once you can present Pastebin cleanly, harder problems become the same method with bigger deep dives.

This walkthrough explains not just what to draw but why, and how a strong candidate talks through each step. If you want the broader learning path first, see the system design roadmap for beginners.

Why Pastebin Is the Ideal First System Design Problem

Pastebin lets a user paste text (a stack trace, a config file, a code snippet), returns a link like paste.example/aB3dE9k, and anyone with the link can view it. Variants: GitHub Gists, Hastebin, and the "share logs with support" feature many products have.

  • Small surface area: two main operations, create and read. You can finish a full design in 45 minutes.
  • Real trade-offs: where to store blobs, how to make keys unique without coordination, how to delete expired data without hurting the database.
  • Transferable: the same ideas power URL shorteners, file sharing, image hosting and document links.

Step 1: Requirements

Spend the first five minutes asking questions. The point is not to collect every possible feature; it is to agree on scope so you can design for it. Here is a reasonable set.

Functional requirements

  1. A user creates a paste with text content and gets back a unique short URL.
  2. Anyone with the URL can read the paste (public or unlisted pastes).
  3. Pastes can have an expiry: 10 minutes, 1 hour, 1 day, 1 month or never. Default: never, or a long default like 1 year.
  4. Optional: custom alias (paste.example/my-config), syntax highlighting, burn after reading, private pastes for logged-in users.
  5. Basic analytics: view count per paste.

Non-functional requirements

  • Durable: once created and before expiry, a paste must not be lost.
  • Highly available reads: links are shared widely; a broken link is the worst outcome.
  • Low latency: reads under ~200 ms globally.
  • Unguessable keys: unlisted pastes should not be discoverable by enumerating URLs.
  • Size limit: maximum paste size 10 MB.
💡
Say what you are not building"I'll skip user accounts, editing and search in the core design and mention them at the end." Explicitly descoping shows judgement and keeps the interview on track.

Step 2: Back-of-the-Envelope Estimates

Assume 1 million new pastes per day and a 10:1 read-to-write ratio. These are assumptions to state, not facts; the interviewer may change them. A day has 86,400 seconds, which beginners can round to 100,000 for quick math.

MetricCalculationResult
Write QPS (average)1,000,000 / 86,400~12 writes/s
Write QPS (peak, 5x)12 × 5~60 writes/s
Read QPS (average)10M / 86,400~116 reads/s
Read QPS (peak, 5x)116 × 5~600 reads/s
Average paste sizeAssumption10 KB
Content per day1M × 10 KB~10 GB/day
Content over 5 years10 GB × 365 × 5~18 TB
Pastes over 5 years1M × 365 × 5~1.8 billion
Metadata over 5 years1.8B × ~300 bytes~550 GB
Read bandwidth (average)116 × 10 KB~1.2 MB/s

What do these numbers tell us? The QPS is small; a few servers handle it. Storage is the interesting dimension: 18 TB of content is too large for a single comfortable relational database, but trivial for object storage. And 550 GB of metadata fits in one well-indexed database, with sharding as a later option. The estimates have already shaped the design. If you want more practice, see back-of-the-envelope estimation.

Step 3: API Design

REST API
POST /api/v1/pastes
  Body: { "content": "...", "expires_in": "1d", "visibility": "unlisted",
          "syntax": "python", "custom_alias": null, "burn_after_read": false }
  201 Created: { "key": "aB3dE9k", "url": "https://paste.example/aB3dE9k",
                 "expires_at": "2026-10-11T10:00:00Z" }
  413 Payload Too Large   (content > 10 MB)
  409 Conflict            (custom alias taken)
  429 Too Many Requests   (rate limited)

GET /api/v1/pastes/{key}
  200 OK: { "key": "...", "content": "...", "syntax": "python",
            "created_at": "...", "expires_at": "...", "views": 1234 }
  404 Not Found   (never existed, expired or burned)

GET /{key}/raw          -> text/plain content, cacheable
DELETE /api/v1/pastes/{key}   (owner only, with auth token or a delete token)

Notice that expired, burned and nonexistent pastes all return the same 404. Returning a different status for "expired" would let someone probe which keys once existed. A separate /raw endpoint returns plain text that a CDN can cache easily and that tools like curl can consume.

Step 4: Object Storage vs Database for Paste Content

This is the central design decision, and the one interviewers most want you to reason about. There are two serious options.

Option A: Store content in the database

One table with a content TEXT column. It is simple and transactional: metadata and content are written together. But 18 TB of mostly cold text bloats the database, makes backups and replicas slow, and wastes expensive database storage and memory on data that is read by key and never queried.

Option B: Content in object storage, metadata in the database

Write the content to S3 (or Google Cloud Storage, Azure Blob, MinIO) under the key, and store a small metadata row pointing to it. Object storage is designed exactly for large, immutable blobs read by key: very cheap per GB, extremely durable, and it scales without capacity planning. How object stores achieve that is covered in Design Object Storage like S3.

CriteriaContent in DBContent in object storage
Cost for 18 TBHigh (DB disks, replicas)Low (object storage pricing)
DB size and backupsHuge, slowMetadata only, ~550 GB
Latency for small pastesOne queryExtra hop (mitigated by cache/CDN)
ConsistencySingle transactionTwo writes; must handle partial failure
Serving through CDNThrough app serversCan serve directly with signed URLs

A sensible hybrid many candidates propose: inline small pastes (under, say, 4 KB) directly in the metadata row and put larger ones in object storage. It saves a hop for the common case. Mention it as an optimisation, but lead with the clean separation.

Handling the two-write problem

Write order matters. Upload the content to object storage first, then insert the metadata row. If the upload succeeds and the insert fails, you have an orphan object that nobody references, which is harmless and can be garbage collected. The opposite order could produce a metadata row pointing to missing content, which is a user-visible broken link.

Step 5: Data Model

SQL
CREATE TABLE pastes (
    paste_key        VARCHAR(16)  PRIMARY KEY,   -- 'aB3dE9k' or a custom alias
    owner_id         BIGINT       NULL,          -- NULL for anonymous
    storage_path     VARCHAR(255) NULL,          -- e.g. 'pastes/aB/aB3dE9k'
    inline_content   TEXT         NULL,          -- small pastes only
    size_bytes       INT          NOT NULL,
    syntax           VARCHAR(32)  NULL,
    visibility       SMALLINT     NOT NULL,      -- 0 public, 1 unlisted, 2 private
    burn_after_read  BOOLEAN      NOT NULL DEFAULT FALSE,
    created_at       TIMESTAMP    NOT NULL,
    expires_at       TIMESTAMP    NULL           -- NULL = never
);
CREATE INDEX idx_pastes_expires_at ON pastes (expires_at);
CREATE INDEX idx_pastes_owner ON pastes (owner_id, created_at);

Why a relational database? The access pattern is simple key lookups, so a key-value store like DynamoDB or Cassandra would also work well, and would make scaling to billions of rows easy. A relational database is fine at 550 GB and gives you the expiry index and owner listing cheaply. Either answer is acceptable if you justify it. The general decision framework is in how to choose a database.

Step 6: Generating Unique Short Keys

The key must be short, unique and hard to guess. Base62 (a–z, A–Z, 0–9) is URL-safe and compact.

626 ≈ 56.8B
6-character keys
627 ≈ 3.52T
7-character keys
~0.05%
Collision chance per new key after 1.8B pastes (7 chars)

We need about 1.8 billion keys over 5 years. Six characters would technically suffice, but random keys need a large space so collisions stay rare and guessing stays impractical. Seven characters give 3.5 trillion: after 1.8 billion pastes, a fresh random key collides with probability 1.8 × 109 / 3.52 × 1012 ≈ 0.05%, so a retry is almost never needed.

Approach 1: Random key + uniqueness check

Generate 7 random base62 characters using a cryptographically secure random source, then insert with the key as primary key. If the insert fails with a duplicate key error, generate a new one and retry. Simple, unguessable and stateless. The database's primary key constraint does the uniqueness check atomically; do not "check then insert" in two steps, which races.

Approach 2: Counter + base62 encoding

Take a globally unique number (from a database sequence, a ticket server or a Snowflake-style generator) and encode it in base62. No collisions ever, but sequential counters produce guessable keys: if your paste is 000aB3x, the next is 000aB3y. That breaks unlisted pastes unless you scramble the number first (for example with a reversible permutation or a block cipher over the integer). Unique ID strategies are compared in depth in Design a Unique ID Generator.

Approach 3: Key Generation Service (KGS)

A separate service pre-generates random unused keys offline and stores them in an "unused keys" table. App servers fetch batches of, say, 1,000 keys, keep them in memory and hand them out. When a key is handed to a server it is marked used, so no two servers get the same key. Writes never wait on a collision check. Trade-off: one more service to run, and keys in a crashed server's memory are wasted (acceptable with trillions available).

Python
import secrets

ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"


def random_key(length: int = 7) -> str:
    """Unguessable key from a CSPRNG."""
    return "".join(secrets.choice(ALPHABET) for _ in range(length))


def base62_encode(n: int) -> str:
    if n == 0:
        return ALPHABET[0]
    out = []
    while n:
        n, r = divmod(n, 62)
        out.append(ALPHABET[r])
    return "".join(reversed(out))


def base62_decode(s: str) -> int:
    n = 0
    for ch in s:
        n = n * 62 + ALPHABET.index(ch)
    return n


def create_paste(db, content_store, content: str) -> str:
    for _ in range(5):                      # retries are almost never needed
        key = random_key()
        content_store.put(f"pastes/{key[:2]}/{key}", content)   # object first
        if db.insert_if_absent(key):        # PK constraint gives atomic uniqueness
            return key
        content_store.delete(f"pastes/{key[:2]}/{key}")
    raise RuntimeError("could not allocate key")


print(base62_encode(125), base62_decode(base62_encode(987654321)))  # 21 987654321
Java
import java.security.SecureRandom;

public class KeyGen {
    private static final String ALPHABET =
        "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final SecureRandom RNG = new SecureRandom();

    static String randomKey(int length) {
        StringBuilder sb = new StringBuilder(length);
        for (int i = 0; i < length; i++) sb.append(ALPHABET.charAt(RNG.nextInt(62)));
        return sb.toString();
    }

    static String encode(long n) {
        if (n == 0) return "0";
        StringBuilder sb = new StringBuilder();
        while (n > 0) { sb.append(ALPHABET.charAt((int) (n % 62))); n /= 62; }
        return sb.reverse().toString();
    }

    public static void main(String[] args) {
        System.out.println(randomKey(7) + " " + encode(125));   // e.g. "aB3dE9k 21"
    }
}

For an interview, Approach 1 is the easiest to defend for Pastebin, because writes are only ~60/s at peak and collisions are negligible. Mention KGS as the answer if the interviewer pushes write volume much higher. Custom aliases bypass generation entirely and rely on the same primary key constraint, returning 409 on conflict.

Step 7: High-Level Architecture

┌──────────────┐ Readers ───────► │ CDN │── cache hit ──► return content └──────┬───────┘ │ miss ▼ Writers ──► ┌──────────────────┐ ┌──────────────┐ │ Load Balancer │ │ Redis cache │◄─┐ └────────┬─────────┘ │ key → paste │ │ ▼ └──────────────┘ │ ┌──────────────────┐ read-through ────────┘ │ Paste API servers│──────────────────────────┐ │ (stateless) │ │ └──┬─────────┬─────┘ ▼ │ │ ┌──────────────────────┐ metadata ▼ ▼ content │ Object storage (S3) │ ┌──────────────┐ put/get ──────────► │ pastes/aB/aB3dE9k │ │ Metadata DB │ └──────────────────────┘ │ (primary + │ ┌───────────────────────────┐ │ replicas) │◄──────│ Cleanup worker (expiry) │ └──────────────┘ └───────────────────────────┘ view events ──► queue ──► analytics aggregator ──► counts table

Write path

  1. Validate. Check size (≤ 10 MB), rate limit by IP or user, and scan for obvious abuse.
  2. Generate key. Random 7-character base62 key.
  3. Store content. PUT to object storage under a prefix derived from the key.
  4. Insert metadata. Insert the row; on duplicate key, retry with a new key.
  5. Return the URL. 201 Created with the key and expiry.

Step 8: Read Path and Caching

Reads dominate, and paste content never changes after creation. Immutable content is the easiest thing in the world to cache, because you never have to invalidate on update, only on delete or expiry.

  1. CDN. Public and unlisted pastes are served through a CDN with Cache-Control max-age set to the smaller of a default (say 1 hour) and the time remaining until expiry. A viral paste is then served from edge locations, never hitting your servers.
  2. Application cache. On CDN miss, API servers check Redis for paste:{key}. Cache metadata plus content for small pastes; for large pastes cache only metadata and stream content from object storage.
  3. Database + object storage. On cache miss, read metadata from a read replica, check expiry, fetch content, populate the cache with a TTL that never exceeds the paste's remaining life.

How big should the cache be? Reads follow a power law: a small fraction of pastes get most views. Caching the hottest 20% of a day's reads: 10M reads/day × 20% × 10 KB ≈ 20 GB, which fits in a modest Redis cluster. Eviction policy: LRU. Cache patterns like read-through and TTLs are explained in designing a distributed cache.

⚠️
Burn-after-read and private pastes are never cachedMark them Cache-Control: private, no-store. For burn-after-read, the read must atomically fetch and delete, for example DELETE FROM pastes WHERE paste_key = ? RETURNING * in PostgreSQL. If two people open the link at the same instant, only one gets the content.

Step 9: Expiry and Cleanup of Old Pastes

Expiry has two separate goals that beginners often merge: users must never see an expired paste (correctness), and expired data must eventually be deleted (cost and privacy). Use a different mechanism for each.

Layer 1: Lazy expiry on read

Every read compares expires_at with the current time. If expired, return 404 immediately (and optionally enqueue a delete). This alone guarantees correctness, even if cleanup is days behind. Cache TTLs are set no longer than the remaining life, so caches cannot serve stale expired content either.

Layer 2: Background cleanup

A cleanup worker runs every few minutes and deletes expired rows in small batches using the expires_at index:

SQL
-- PostgreSQL: delete in bounded batches to avoid long locks and replica lag
WITH expired AS (
    SELECT paste_key, storage_path
    FROM pastes
    WHERE expires_at < now()
    ORDER BY expires_at
    LIMIT 1000
    FOR UPDATE SKIP LOCKED
)
DELETE FROM pastes p
USING expired e
WHERE p.paste_key = e.paste_key
RETURNING e.storage_path;    -- worker then deletes these objects from storage

Why batches? A single DELETE ... WHERE expires_at < now() over millions of rows holds locks for a long time, bloats the write-ahead log and makes replicas lag. SKIP LOCKED lets several workers run in parallel without fighting over the same rows.

Cheaper alternatives

  • Database-native TTL: DynamoDB TTL, Cassandra TTL or MongoDB TTL indexes delete expired items automatically (DynamoDB documents that deletion typically happens within a few days, which is why the read-time check is still needed).
  • Object storage lifecycle rules: store content under a prefix per expiry bucket (exp-1d/, exp-30d/) and let a lifecycle rule delete objects older than N days with no worker at all.
  • Time-partitioned tables: partition metadata by expiry date and drop whole partitions, which is far cheaper than row deletes.

Step 10: View Counts and Analytics

A naive design runs UPDATE pastes SET views = views + 1 on every read. That turns your read-heavy system into a write-heavy one, creates row-lock contention on viral pastes and stops CDN-served reads from being counted at all.

Better: on each read (including CDN logs for edge hits), emit a small event {key, ts, country, referrer} to a queue such as Kafka. An aggregator consumes events, sums counts per key per minute in memory and writes one batched update per key per minute to a counts table. View counts become eventually consistent by about a minute, which nobody minds. For deduplicating "unique viewers", a HyperLogLog per paste estimates distinct visitors in about 12 KB of memory per key in Redis.

Step 11: Abuse, Privacy and Security

  • Rate limiting: anonymous users get, say, 10 pastes per minute per IP; logged-in users more. A token bucket at the gateway is standard; see rate limiter system design.
  • Content scanning: paste sites attract leaked credentials and malware payloads. Run async scanners for API keys, card numbers and known malware signatures; flagged pastes are hidden pending review.
  • Unguessable keys: CSPRNG keys, no sequential IDs for unlisted pastes, and no public listing of unlisted pastes.
  • XSS: always render paste content as escaped text, serve raw content with Content-Type: text/plain and X-Content-Type-Options: nosniff, ideally from a separate domain.
  • Encryption: object storage encryption at rest by default. For true privacy, some paste tools encrypt in the browser and put the key in the URL fragment (after #), which browsers never send to the server.

Step 12: Scaling Further and Trade-offs

Growth scenarioBottleneckFix
Reads grow 100xApp servers, DB replicasHigher CDN hit rate, bigger Redis, more read replicas
Metadata grows past a few TBSingle DB primaryShard by hash of paste_key, or move to DynamoDB/Cassandra
Writes grow 100xKey collision retries, DB primaryKGS for keys, sharded metadata
Global usersLatency from one regionCDN everywhere; replicate object storage across regions
One viral pasteHot key in cacheCDN absorbs it; local in-process cache on API servers

The trade-offs worth stating aloud: object storage adds a hop but saves enormous database cost; random keys need a uniqueness check but are unguessable; lazy expiry is instant and correct but needs a background job to actually free space; asynchronous analytics are cheap but slightly delayed.

How to Present Pastebin in a 45-Minute Interview

0–5 min

Requirements

Create, read, expiry, unlisted, size limit. Descope accounts and editing.

5–10 min

Estimates

QPS, 18 TB of content, 1.8B keys. Conclude: object storage plus small metadata DB.

10–25 min

API, data model, architecture

Draw the diagram, walk write and read paths, justify storage split.

25–40 min

Deep dives

Key generation, caching with immutable content, expiry layers, analytics.

40–45 min

Wrap-up

Bottlenecks, scaling path, abuse controls, what you would add next.

Common beginner mistakes

  • Jumping into microservices and Kafka before stating requirements.
  • Storing 18 TB of text in MySQL without discussing the alternative.
  • Using an auto-increment ID as the public key, making unlisted pastes enumerable.
  • "Check if key exists, then insert" as two steps, which races under concurrency.
  • Relying only on a cron job for expiry, so expired pastes stay visible until it runs.
  • Updating a view counter in the database on every read.

Master this one and you will notice the same skeleton in your next problems. Natural follow-ups are a file-sharing service like Google Drive / Dropbox and, for practice with similar trade-offs at larger scale, the curated list of system design interview questions.

Frequently Asked Questions

What is the Pastebin system design question?▾
You design a service where users paste text, get a short unique link and others open that link to read the text. It tests requirements gathering, estimation, key generation, storage choices, caching and expiry, which makes it a classic first system design problem.
Should Pastebin store paste content in a database or object storage?▾
Store the content in object storage such as Amazon S3 and keep only small metadata (key, owner, expiry, size, storage location) in a database. Content is large, immutable and read by key, which object storage handles cheaply, while the database stays small and fast.
How do you generate short keys for Pastebin?▾
Common options are random base62 strings with a uniqueness check, encoding a distributed counter in base62, or a key generation service that pre-creates unused keys. Seven base62 characters give about 3.5 trillion combinations, enough for billions of pastes.
How are expired pastes deleted?▾
Use two layers. On every read, check the expiry time and return 404 if it has passed, so users never see expired content. Separately, a background job or storage lifecycle rule deletes expired metadata and objects in batches during off-peak hours.
How is Pastebin different from a URL shortener?▾
Both map a short key to something. A URL shortener stores a tiny URL and redirects, while Pastebin stores user content of up to several megabytes, so storage design, size limits, content caching and abuse scanning matter much more.
Is Pastebin read-heavy or write-heavy?▾
It is read-heavy. A typical assumption is around 10 reads per write, and popular pastes can be read millions of times, which is why a CDN and a cache in front of storage matter more than write optimisation.
How do you implement burn-after-reading pastes?▾
The first read must atomically fetch and delete the paste so two simultaneous readers cannot both see it. A conditional delete in the database or an atomic get-and-delete in Redis works, and such pastes must never be cached by a CDN.

Start System Design the Right Way

Prepflix's system design course starts with problems like Pastebin and builds up to Uber and Kafka, with mock interviews at every step.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.