Design Pastebin
The interviewer asks
“Design a service like Pastebin or GitHub Gist where users paste text or code, get a unique URL, and anyone with the URL can view it. Pastes can expire and can be public or unlisted.”
What a strong answer looks like
Pastebin looks like a URL shortener, but the key difference is that the payload is large (kilobytes to megabytes), so the strong answer separates metadata from content: small metadata rows go in a database, the paste body goes in object storage (S3) addressed by key, and reads are served through a CDN because pastes are immutable. Generate unguessable keys (unlisted pastes must not be enumerable), enforce size limits, handle expiry with TTLs and lifecycle rules, and protect the system from abuse (spam, malware, leaked secrets). A good candidate also discusses small-object cost on S3, compression, syntax highlighting on the client, and view counting off the read path.
Concepts to mention
1 Requirements
Functional
- Create a paste with text content, optional title, language for syntax highlighting, visibility (public/unlisted) and expiry (10 min, 1 day, 1 month, never)
- Get a unique URL like paste.io/k7Qm2xPa and view the paste as HTML or raw text
- Optional 'burn after reading': the paste is deleted after the first view
- Logged-in users can list and delete their own pastes
- Show a view count per paste
- Recent public pastes page
Non-functional
- Read latency p99 under 100 ms for pastes up to 100 KB (served from CDN cache most of the time)
- Availability 99.9% for reads; content must never be lost before its expiry (11 nines durability via S3)
- Scale: 10M new pastes/day, 100M reads/day (10:1 read:write)
- Max paste size 10 MB; average around 10 KB
- Unlisted paste URLs must not be guessable or listable
Out of scope (say it out loud)
- Real-time collaborative editing
- Version history and forks (mention as an extension)
- Full-text search across all pastes
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
POST/v1/pastes | Body: {content, title?, language?, visibility, expires_in?, burn_after_read?}. Returns {key, url}. Rejects bodies over 10 MB. |
GET/{key} | HTML view with syntax highlighting (rendered on the client). |
GET/raw/{key} | Raw text, served from CDN with long cache headers (immutable). |
GET/v1/users/me/pastes?cursor= | Paginated list of the user's pastes. |
DELETE/v1/pastes/{key} | Owner deletes a paste; removes metadata, S3 object and purges CDN. |
4 Data model
paste_metadataPostgres (sharded by key) or DynamoDBkey (PK)owner_idtitlelanguagevisibilitysize_bytescontent_hashs3_keycreated_atexpires_atburn_after_readSmall rows, key lookups plus 'list by owner' (index on owner_id, created_at). 1.8 TB/year so plan sharding by key or use DynamoDB with a GSI on owner_id.
paste_contentS3object key = pastes/{key}body gzip-compressedContent-Type text/plainDurable, cheap, unlimited. Lifecycle rules move old objects to Infrequent Access after 30 days. Very small pastes (<4 KB) may be stored inline in the metadata row to save S3 request cost.
view_countsRedis + periodic flush to DBkey -> counterINCR in Redis on view; a job flushes to the metadata table every minute. Approximate is fine.
recent_publicRedis sorted setscore = created_atmember = key (last 1,000)Feeds the 'recent pastes' page; trimmed with ZREMRANGEBYRANK.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Create a paste
- 1Client POSTs content (up to 10 MB) with visibility and expiry.
- 2Write service checks size, rate limit per IP/user (e.g. 20 pastes/min) and generates a random 8-char base62 key from a CSPRNG.
- 3It compresses the body (zstd) and PUTs it to S3 at pastes/{key}; S3 confirms durable write.
- 4It inserts the metadata row with a 'key does not exist' condition; on the rare conflict it regenerates the key and retries.
- 5It publishes a 'paste created' event to SQS and returns the URL to the client.
- 6The scanner consumes the event, checks for malware links, spam patterns and leaked secrets (AWS keys, private keys) and marks the paste hidden if needed.
View a paste
- 1Browser requests /raw/{key}; the CDN returns cached content if present (pastes are immutable, so cache for hours).
- 2On a miss the read service fetches metadata from Redis or the DB and checks expiry, visibility and status.
- 3It fetches the content from S3, decompresses or passes gzip through, and returns it with Cache-Control set to the remaining lifetime.
- 4It increments the view counter in Redis; a job flushes counts to the DB every minute.
- 5For burn-after-read pastes, it serves with no-store, and atomically marks the paste consumed (conditional update) before returning, so only one reader wins.
7 Deep dives
Separating metadata from content
Putting 10 KB to 10 MB blobs into a relational database works at first but hurts later: rows become huge, backups and replication slow down, the buffer cache fills with paste bodies instead of indexes, and storing 36 TB/year in Postgres is expensive. Object storage is built for this: 11 nines of durability, no capacity planning, cheap per GB, and lifecycle rules for tiering. The metadata row stays small (~500 B) and holds the S3 key, size and content hash, so listing, expiry and permission checks never touch S3. One trade-off to raise: S3 charges per request (about $0.005 per 1,000 PUTs), and at 10M pastes/day that is ~$50/day just for PUTs. Many pastes are under 1-2 KB, so a common optimisation is to store small bodies inline in the metadata row and only use S3 above a threshold such as 4 KB. Another is content-hash de-duplication: if two pastes have the same SHA-256, they can point to the same object (with reference counting for deletes). Mention write order: write S3 first, then metadata. If the metadata write fails you leave an orphan object, which a periodic sweep removes; the reverse order would create metadata pointing to nothing.
Expiry and cleanup at scale
Expiry has two parts: making expired pastes invisible immediately, and actually deleting the bytes later. Visibility is easy: the read path checks expires_at on every request, and the CDN Cache-Control max-age is set to the remaining lifetime, so the edge never serves a paste past expiry. Deletion is a background job. With DynamoDB you get native TTL that deletes rows (within roughly 48 hours) and emits a stream event that a Lambda uses to delete the S3 object. With Postgres, a job scans an index on expires_at in small batches (DELETE ... WHERE expires_at < now() LIMIT 5000) to avoid long locks and replication lag. For S3, you can also use prefixes per expiry bucket (e.g. expires/2026-10-25/) with lifecycle rules that delete whole prefixes, which avoids millions of single DELETE calls. Burn-after-read is a special case: use a conditional update (status from active to consumed) so two simultaneous readers cannot both see it, and do not let the CDN cache it.
Abuse: spam, malware and leaked secrets
Anonymous paste sites are abused for phishing payloads, malware command-and-control configs and dumps of stolen data. A real design needs: rate limits per IP and per account (token bucket in Redis), CAPTCHA after suspicious bursts, and an asynchronous scanner that checks URLs against Safe Browsing, runs spam classifiers, and detects secrets with regex and entropy rules similar to GitHub secret scanning (AWS access keys start with AKIA, private keys have PEM headers). When a secret is found in a public paste, hide it and optionally notify the provider. Keep the scan asynchronous so creates stay fast, but make reads check a 'hidden' flag. Also rate limit 404s per IP, because a scraper guessing keys produces mostly 404s; that is the signal to block it.
8 Trade-offs
Random keys vs counter-based keys
Unlisted pastes rely on the key being secret, so random 8-character keys from a cryptographic generator are better than counters, even scrambled ones. Collisions are rare (billions used out of 218 trillion) and handled by a conditional insert and retry. Counter-based keys are a fine choice for URL shorteners where links are meant to be shared publicly.
Server-side vs client-side syntax highlighting
Rendering highlighted HTML on the server costs CPU on every cache miss and makes the response bigger. Sending raw text plus a JavaScript highlighter (Prism, highlight.js) moves the work to the browser and lets the CDN cache one immutable raw object. Server-side rendering is only worth it for SEO of public pastes.
CDN caching vs instant deletion
Immutable content is perfect for long CDN caching, but an owner deleting a paste, or a takedown, needs it gone now. Cache with a moderate max-age (e.g. 1 hour) plus explicit purge by URL on delete. Purge APIs complete in seconds on modern CDNs.
9 Bottlenecks
Viral paste
A paste linked from a big forum can get 10k+ reads/s. Because it is immutable, the CDN absorbs nearly all of it. Make sure view counting uses Redis INCR (or sampled CDN logs) instead of updating the DB row per view, which would create a hot row.
Metadata DB growth
Billions of rows over years. Shard by key (hash), or use DynamoDB. Delete expired rows continuously, and archive 'never expire' pastes that have not been read in a year to a cheaper tier with their metadata.
10 Failure handling
S3 write succeeds, metadata write fails
The client gets an error and retries with a new key. The orphan S3 object is invisible because nothing points to it. A weekly reconciliation job lists objects older than a day without metadata and deletes them.
S3 regional outage
Reads of already-cached pastes continue from the CDN. Enable cross-region replication for content if the SLA requires it; otherwise accept degraded reads for uncached pastes and queue creates locally (or fail creates with a clear error).
11 What impresses
- Explains why blobs do not belong in the relational DB with concrete numbers (36 TB/year)
- Notices S3 per-request cost and inlines tiny pastes
- Sets CDN max-age from the remaining paste lifetime so expiry is respected at the edge
- Handles burn-after-read races with a conditional update
- Brings up secret scanning and phishing as real operational problems
- Defines the write order (S3 then metadata) and how orphans are cleaned
12 Mistakes
- Storing paste bodies in the same SQL table as metadata at multi-TB scale
- Using sequential IDs, making unlisted pastes trivially enumerable
- No size limit, allowing one user to upload gigabytes
- Updating a view counter row in the database on every read
- Forgetting how expired content is actually deleted
13 Follow-ups
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.