- 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
- A user creates a paste with text content and gets back a unique short URL.
- Anyone with the URL can read the paste (public or unlisted pastes).
- Pastes can have an expiry: 10 minutes, 1 hour, 1 day, 1 month or never. Default: never, or a long default like 1 year.
- Optional: custom alias (
paste.example/my-config), syntax highlighting, burn after reading, private pastes for logged-in users. - 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.
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.
| Metric | Calculation | Result |
|---|---|---|
| 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 size | Assumption | 10 KB |
| Content per day | 1M × 10 KB | ~10 GB/day |
| Content over 5 years | 10 GB × 365 × 5 | ~18 TB |
| Pastes over 5 years | 1M × 365 × 5 | ~1.8 billion |
| Metadata over 5 years | 1.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
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.
| Criteria | Content in DB | Content in object storage |
|---|---|---|
| Cost for 18 TB | High (DB disks, replicas) | Low (object storage pricing) |
| DB size and backups | Huge, slow | Metadata only, ~550 GB |
| Latency for small pastes | One query | Extra hop (mitigated by cache/CDN) |
| Consistency | Single transaction | Two writes; must handle partial failure |
| Serving through CDN | Through app servers | Can 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
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.
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).
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
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
Write path
- Validate. Check size (≤ 10 MB), rate limit by IP or user, and scan for obvious abuse.
- Generate key. Random 7-character base62 key.
- Store content. PUT to object storage under a prefix derived from the key.
- Insert metadata. Insert the row; on duplicate key, retry with a new key.
- 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.
- CDN. Public and unlisted pastes are served through a CDN with
Cache-Controlmax-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. - 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. - 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.
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:
-- 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/plainandX-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 scenario | Bottleneck | Fix |
|---|---|---|
| Reads grow 100x | App servers, DB replicas | Higher CDN hit rate, bigger Redis, more read replicas |
| Metadata grows past a few TB | Single DB primary | Shard by hash of paste_key, or move to DynamoDB/Cassandra |
| Writes grow 100x | Key collision retries, DB primary | KGS for keys, sharded metadata |
| Global users | Latency from one region | CDN everywhere; replicate object storage across regions |
| One viral paste | Hot key in cache | CDN 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
Requirements
Create, read, expiry, unlisted, size limit. Descope accounts and editing.
Estimates
QPS, 18 TB of content, 1.8B keys. Conclude: object storage plus small metadata DB.
API, data model, architecture
Draw the diagram, walk write and read paths, justify storage split.
Deep dives
Key generation, caching with immutable content, expiry layers, analytics.
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
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 →