TSTier SlateAll templates

Infrastructure primitives

Pastebin - a blob store with a short name

Why storing text is the easy part and expiry, abuse and the read path are the design.

  • System design
  • starter
  • 8 step walkthrough
  • 3 tables

Share

  • Facebook
  • X

What this board gets wrong on purpose

The tension

Expiry is a sweeper that deletes rows in batches every few minutes, and the CDN caches each paste for an hour, so a paste marked "expires in 10 minutes" can stay readable for well over an hour after it should have died - only the origin checks expires_at on every read. Burn-after-reading cannot be cached at all and still goes through the same cached path. The abuse scanner runs off a queue, so a malware paste is served from the moment it is created until the scan finishes, and a takedown only reaches the edge if somebody remembers to purge it. And the text and its row are written to two stores with no transaction between them, so a crash in between leaves orphaned blobs or rows pointing at nothing.

Practise this as an interview

Take Pastebin - a blob store with a short name as a timed system design interview on a blank board - on your own, with the requirements to check yourself against, or with a candidate you invite.

Practise this as an interview

Start here

Why this design

Pastebin is a common early interview question because it looks like a URL shortener with a bigger value, and the interesting answer is the part that is not. Storing text is trivial: put it in an object store, keep a small row about it, hand back an id. The design is in what happens next - pastes expire, pastes are abused, and pastes are read from caches that do not know either of those things. The same shape appears in gists, shared snippets, crash-report uploads, temporary file shares and "share this config" links.

What you will learn

  • Why the text goes in an object store and only ~300 bytes of metadata go in the database - and what that split costs.
  • How to size it: 2M pastes a day is only ~23 writes/s, but 16 GB of new text a day and ~2,300 reads/s at peak.
  • Why ids are random 8 character base62 strings (62^8 ≈ 2.2e14) rather than a counter: unlisted pastes must not be guessable.
  • Lazy expiry on read against an expiry sweeper - and why you need both.
  • Why immutable text is easy to cache and why that same cache defeats expiry, burn-after-reading and takedowns.

How to read this board

  1. WRITE PATH across the top: author, client, the Paste API hex, the scan queue and the abuse scanner.
  2. STORES on the right: paste metadata (the row), paste text (the blob) and the hot paste cache.
  3. READ PATH across the bottom: reader, CDN edge, the read service hex, and the response going back.
  4. TAKING IT AWAY on the right: the red expiry sweeper and moderation, the two things that try to make a paste unreadable.
  5. Press play for the walkthrough, then read pages 1 to 6, then the DATA MODEL tables under the pages.
  6. The conversation to have: every arrow that removes a paste is slower than the arrow that serves it. Decide which promise - expiry, burn-after-reading or takedown - you would fix first, and what it costs the cache.

1 · Requirements

Functional

  • Create: POST text (up to 512 KB) with an optional title, syntax language, visibility (public or unlisted) and expiry (10 minutes, 1 hour, 1 day, 1 week, never, or burn after reading). Returns https://paste.example/{id}.
  • Read: GET /{id} returns the paste rendered with syntax highlighting; GET /raw/{id} returns text/plain.
  • Anonymous pastes are allowed; signed-in users can list and delete their own.
  • An expired, deleted or taken-down paste returns 404 (or 410 Gone) - never the old text.
  • Anyone can report a paste; moderators can take it down.

Non-functional

  • Read latency: p99 under 100 ms for a cached paste, under 300 ms for one fetched from the object store.
  • Read availability 99.95 percent; create availability 99.9 percent.
  • Durability: a paste that was acknowledged is never lost before its expiry (object stores give 11 nines).
  • Unlisted ids must be unguessable: no sequential ids, and no listing endpoint that reveals them.
  • Expiry is a promise: an expired paste should be unreadable everywhere within a stated bound (say 5 minutes).
  • Abuse controls: per-IP create rate limits, a size cap, and scanning for credentials and malware.

Out of scope

  • Editing a paste in place. A new version is a new paste; immutability is what makes the cache simple.
  • Full-text search across public pastes - a separate indexing pipeline, and an abuse magnet of its own.
  • Collaboration, comments and forks.

2 · Capacity estimation

Assumptions

New pastes per day                  2,000,000
Average paste size                       8 KB   (cap 512 KB)
Reads per paste, lifetime                  20
Reads per day      2M x 20       = 40,000,000
Peak factor        writes x 3, reads x 5

Requests per second

Writes   2M / 86,400 s        =     23 /s average
         x 3                  =    ~70 /s peak

Reads    40M / 86,400 s       =    463 /s average
         x 5                  = ~2,300 /s peak

One paste linked from a big forum thread
                              =  5,000 /s on ONE id

These are small numbers. A single modest database handles 70 inserts a second without noticing, and the read rate is well within one cache node. The numbers that matter are bytes, not requests - and the one viral paste, which is why there is a CDN at all.

Bandwidth

Ingress   23 /s x 8 KB        =   184 KB/s average
Egress    463 /s x 8 KB       =   3.7 MB/s average
          2,300 /s x 8 KB     =  18.4 MB/s peak  (~150 Mbit/s)
gzip on text, ~3:1            =   ~6 MB/s peak on the wire

Cache sizing

Most reads land in the first 24 hours of a paste
Pastes created per day         2M x 8 KB  =  16 GB
Cache the hottest 20 percent              = ~3.2 GB

A few gigabytes of hot text fits in one cache node with room to spare. It is replicated for availability, not for size.

3 · Storage estimation

Two things per paste

Metadata row (database)
  id 8 B, owner 16 B, blob key ~40 B, size, sha256 32 B,
  syntax, visibility, flags, created/expires 16 B
                                  ~150 B of data
  + index and row overhead        ~300 B per paste

Text (object store)               8 KB average

Growth

Text per day        2M x 8 KB          =   16 GB
Text per year       16 GB x 365        =  5.8 TB written
Never-expire share  40 percent
Retained per year   5.8 TB x 40%       =  2.3 TB

Metadata per year   730M x 300 B       =  219 GB written
Retained per year   x 40%              =   88 GB

This is why the text does not go in the database. Twenty-three terabytes of text after a decade is cheap in an object store (a few hundred dollars a month) and painful in a relational database, where it would bloat every backup and replica. The metadata, under a terabyte after ten years, fits one well-indexed Postgres primary with replicas.

Id space

Alphabet [0-9a-zA-Z]                 = 62
Length 8  62^8                       = 2.2e14
10 years of pastes                   = 7.3e9
Share of the space used              = 0.003 percent
Chance a random guess hits a paste   = 1 in 30,000

Random ids are generated by the API and inserted with a unique constraint; a collision (vanishingly rare) is simply retried. One in 30,000 is fine against a casual guess but not against a scraper making millions of requests, which is why reads are rate limited per IP too.

Dedup

Storing the SHA-256 of the text lets identical pastes share one blob, which matters for the abuse case: the same credential dump posted ten thousand times is one blob and one takedown. The cost is that a blob can only be deleted once no live row points at it.

4 · Availability

What fails, and what it costs

  • Object store. Regional, highly available, 11 nines of durability. If it is slow, uncached reads are slow; cached reads continue.
  • Metadata database. One primary with two replicas. Losing the primary stops creates until failover (30-60 s); reads continue from replicas and cache.
  • Hot paste cache. Losing it costs latency, never correctness - every miss falls through to the replica and the object store, which at 2,300 reads/s is well within their limits.
  • CDN. If the edge is down or bypassed, the origin sees every read. Size the read service for the uncached peak.
  • Scan queue. If it backs up, pastes are served unscanned for longer. Nothing breaks visibly, which is exactly why it needs an alert on queue age.
  • Expiry sweeper (red). If it stops, nothing is ever deleted - storage grows and, if the read path forgets to check expires_at, expired text is served.

The write is two writes

The API writes the blob first, then the row. If it crashes between them, the blob is an orphan: harmless but billed. Writing the row first would be worse - a row pointing at nothing is a paste that 500s. So: blob first, row second, and a weekly job that lists blobs with no row older than a day and deletes them.

What degrades instead of dying

  • Scanner down: pastes go live unscanned and are scanned when it recovers.
  • Primary down: creates fail fast with a retryable error; reads are unaffected.
  • Cache down: reads slow from ~5 ms to ~40 ms, nobody sees an error.

5 · How it works

Write path

The author submits text. The Paste API checks the size cap and the caller’s rate limit, generates a random 8 character id, gzips the text and PUTs it to the object store under a key derived from its SHA-256. It then inserts the metadata row - id, blob key, size, syntax, visibility and expires_at - and returns the link. Finally it drops a scan job on the queue. The author has their link in about 50 ms; the scan happens after.

Read path

A reader opens the link. The CDN edge serves it if cached. On a miss the read service looks in the hot paste cache, and on a cache miss reads the row from a replica and the text from the object store. The read service checks the row on every origin read: status must be active and expires_at must be in the future, otherwise 404. Text is immutable, so the response carries Cache-Control: public, max-age=3600 - and that header is the whole tension.

Expiry, twice

  • Lazy expiry: every origin read compares expires_at to now. This makes the origin correct the instant a paste expires.
  • The sweeper: every few minutes it deletes rows WHERE expires_at < now() in batches of 10,000 using the index on expires_at, then deletes blobs no live row references. This reclaims space; it is not what makes expiry correct.
  • The edge: neither of the above touches the CDN. Bound it by setting max-age to min(3600, seconds until expiry), and send Cache-Control: no-store for burn-after-reading.

Burn after reading

The first read must atomically flip the row to burned (UPDATE ... SET status = burned WHERE id = $1 AND status = active RETURNING blob_key) and only the request that wins that update gets the text. It must never be cached anywhere - not at the edge, not in the hot cache - and link-preview bots that fetch URLs pasted into chat will "read" it first unless the page requires a click to reveal.

Abuse

The scanner matches text against credential patterns (emails with passwords, API key formats), known-malware hashes and blocklisted domains, and marks the row flagged. Moderation takes a paste down by setting status, then must purge its URLs from the CDN. A takedown that skips the purge keeps serving for up to an hour.

6 · Deployment plan

Topology

  • One region is enough for these numbers. Paste API and read service are stateless containers behind a load balancer, at least two per zone across two zones.
  • Postgres primary with two replicas in different zones; reads go to replicas, with the author’s first read pinned to the primary so a fresh paste is never a 404.
  • Object store bucket with versioning off and a lifecycle rule as a backstop: objects under the expiring/ prefix older than 8 days are deleted even if the sweeper fails.
  • A managed CDN in front of the read service only. The API host is never cached.
  • Sweeper and scanner as separate worker deployments, so a slow scanner never delays deletion.

Rollout

  • Read service: canary 5 percent, watch p99, 404 rate and 5xx rate.
  • Cache-Control changes ship behind a flag and start short: a header sent with max-age=86400 cannot be recalled from caches that already have it.
  • Id length and alphabet are fixed forever - links are pasted into chat logs and bug trackers for years.

What to watch

Scan queue age (how long pastes are live unscanned), sweeper lag (oldest row past its expires_at), orphan blob count, CDN hit ratio, and create rate per IP - a sudden spike from a handful of addresses is somebody using you as free hosting.

The walkthrough

  1. Somebody pastes text

    An author pastes a stack trace into the form and picks "expires in 1 day". Up to half a megabyte of text, and an expiry that is a promise.

  2. The API checks and names it

    The Paste API checks the size cap and the caller’s rate limit, then generates a random 8 character id. Random, not a counter, because an unlisted paste is only private for as long as nobody can guess its id.

  3. Text to the blob store, row to the database

    The text goes to the object store, gzipped, under its SHA-256. Then a 300 byte row goes into Postgres with the id, the blob key and expires_at. Two writes, no transaction: blob first, so a crash leaves a harmless orphan instead of a row pointing at nothing.

  4. Scanned afterwards

    The author already has the link. Only now does a scan job reach the abuse scanner, which looks for credential dumps and malware. Until it finishes, whatever was pasted is live.

  5. A reader opens the link

    The link gets shared. The first request reaches the CDN edge; for a paste somebody posted on a busy forum, almost all of the following requests stop here.

  6. The origin checks expiry

    On an edge miss the read service tries the hot cache, then the row and the blob. It checks status and expires_at on every read, so the origin never serves an expired paste.

  7. Served, and cached for an hour

    The text goes back with max-age=3600, because text never changes. Now the edge answers for this paste by itself - and it does not know what expires_at is.

  8. Taking it away is slower

    Every few minutes the sweeper deletes expired rows and their blobs, and moderation takes down what the scanner flagged. Neither reaches the copy at the edge unless somebody purges it, so an expired or taken-down paste keeps being served until the cache entry runs out.

The data model

  • users

    Only signed-in authors. Anonymous pastes have no owner.

    • iduuidprimary key
    • emailvarcharuniquenot null
    • created_attimestamptznot null
    • bannedbooleannot nullSet by moderation; blocks new pastes.
  • pastes

    One row per paste. The text itself lives in the object store under blob_key; this row is what the read service checks on every origin read.

    • idvarcharprimary keyRandom base62, 8 characters. Never sequential: unlisted pastes must not be guessable.
    • owner_iduuidNull for anonymous pastes.
    • blob_keytextnot nullObject store key, derived from content_sha256 so identical pastes share a blob.
    • content_sha256byteanot null32 raw bytes.
    • size_bytesintegernot nullUncompressed; capped at 524288.
    • syntaxvarchar
    • visibilityvarcharnot nullpublic | unlisted
    • burn_after_readbooleannot nullNever cached; first read flips status to burned.
    • statusvarcharnot nullactive | flagged | taken_down | burned
    • created_attimestamptznot null
    • expires_attimestamptzNull means never. Checked on read; swept by index.
  • abuse_reports

    What moderation works from. Deleted with the paste when the sweeper removes it (ON DELETE CASCADE), so anything needed for an audit trail is copied out at resolution time.

    • idbigserialprimary key
    • paste_idvarcharnot null
    • reasonvarcharnot nullcredentials | malware | phishing | other
    • sourcevarcharnot nullscanner | user
    • created_attimestamptznot null
    • resolved_attimestamptz

On the board

  • WRITE PATH · PASTE SOME TEXT

    • Author
    • Paste form · web, curl, editor plugin
    • Paste API · POST /pastes
    • Scan jobs · after the link is returned
    • Abuse scanner · creds, malware, blocklist
  • STORES

    • Paste metadata · Postgres · id, expiry, status
    • Paste text · object store · gzip, by sha256
    • Hot paste cache · Redis · first 24 h of reads
  • READ PATH · OPEN THE LINK

    • Reader
    • CDN edge · max-age=3600
    • Read service · GET /{id} · checks expiry
  • TAKING IT AWAY

    • Expiry sweeper · every 5 min, 10k rows a batch
    • Moderation · reports -> takedown

More boards

The building blocks an interview asks you to build from scratch.

  • URL shortener - one write, a million reads
  • Distributed cache - the ring, and what a node leaving costs
  • Key-value store - quorums, and where the consistency knob sits
  • Rate limiter - token bucket, and where the counter lives
  • Unique ID generator - 64 bits, and two facts nobody can check
  • Web crawler - a frontier, and the trap of crawling forever
  • Load balancer - spreading traffic, and noticing who is dead
  • API gateway - one front door for many services
  • Login and sessions - who you are, on every request
  • Feature flags - a config change that ships like code
  • Secrets manager - the one store everything trusts
  • DNS - the cache that runs the internet
  • Distributed cron - exactly one runner at nine o'clock
  • Background job queue - at least once, and the retry that doubles it
  • Code execution sandbox - running a stranger's program
  • Distributed log - Kafka from the partition up
  • Consensus - a leader, a log, and a majority
  • Object storage - eleven nines from cheap disks
All 100 templatesOpen Tierslate
Tierslate

tierslate.com

HomeTemplatesPrivacyTerms