Design a Pastebin (System Design)
Table of Contents + −
You’ve probably done this while pairing with a friend:
- You have a chunk of code or an error log that’s way too big to paste into a chat.
- So you drop it into a site like Pastebin, hit save, and out pops a short link like
paste.ly/aZ3kP. - You send that link over, your friend clicks it, and they see the exact same text you pasted.
That little tool is a lovely system design interview question. It looks tiny, but it quietly pulls in databases, object storage, caching, expiry, and read-heavy scaling. So let’s build one together, step by step.
🎯 What We’re Building
So what exactly is a Pastebin? Let’s name it plainly first.
- A Pastebin is a service where you save a blob of text and get back a short link to share it.
- The link ends in a short tag like
aZ3kP. We’ll call that the key, it’s how we find your paste again. - Anyone with the link can open it and read the text. No login needed for the basic version.
Now why would anyone want this? A few everyday reasons:
- Big code or logs clutter up a chat window. A link keeps the conversation clean.
- You can share the same snippet with many people by sending one link.
- You can set a paste to disappear after a while, which is handy for secrets you don’t want lying around forever.
So our job is to do two things really well: save a piece of text, and hand it back fast when someone opens the link. Simple to say, but the fun is in doing it at scale.
📋 Requirements
Before drawing any boxes, a good engineer asks: what must this thing actually do? We split that into two buckets.
- A functional requirement is something the system must do, a feature you can point at.
- A non-functional requirement is about how well it does those things, like how fast or how reliable it is.
Here’s what our Pastebin must do. These are the functional ones:
- Create a paste: take some text and give back a short key (and a link).
- Read a paste: when someone opens the link, return the original text.
- Optionally let a paste expire after some time.
- Optionally let a user pick their own custom key, like
paste.ly/my-notes.
And here’s how well it should do them. These are the non-functional ones:
- Reads should be fast, because people are waiting on a click. A slow paste feels broken.
- The service should be highly available, meaning it stays up almost all the time.
- It should scale, so it keeps working even when millions of pastes and clicks pile on.
Always ask before you design
In a real interview, don’t jump straight to boxes. First ask what features matter and roughly how big it needs to be. Nailing the requirements first is half the score.
📊 Rough Scale
Now let’s get a rough feel for the size. This is a back-of-the-envelope estimate, which just means quick, rough math to guess how big things are. We want the shape, not exact numbers.
- Think about how the service gets used. Saving a paste happens once. That’s a write, meaning we store something new.
- But that one paste might get opened many times after that. Each open is a read, meaning we just look something up.
- So reads hugely outnumber writes. Just like a URL shortener, a paste is created once and viewed lots of times.
There’s a twist here that the URL shortener didn’t have:
- A short URL stores a tiny string. A paste can store a big blob of text, like a whole log file or a long program.
- So our system is read-heavy, and it also has to handle chunks of text that can get large.
Keep both facts in your head: reads dominate, and the text can be big. They shape almost every choice we make next.
🔌 The API
Let’s pin down how the outside world talks to our service. The way other programs talk to ours is called an API, short for Application Programming Interface. It’s just the agreed set of requests we accept.
We really only need two endpoints. Here’s what they look like.
POST /pasteBody: { "text": "console.log('hello')", "expiry": "1d" }Response: { "key": "aZ3kP", "url": "https://paste.ly/aZ3kP" }
GET /aZ3kPResponse: 200 OK -> { "text": "console.log('hello')" }Let’s read them one at a time:
POST /pasteis the create call. You hand it the text and an optional expiry, and it hands back a key and a link. We usePOSTbecause we’re creating something new on the server.GET /aZ3kPis the read call. Someone opens the link, and our server returns the saved text.- The
expiryfield is optional. Here1dmeans “delete this after one day”. If you leave it out, the paste just sticks around.
Why two simple endpoints are enough
The basic Pastebin really is just save-and-fetch. One call to write, one call to read by key. Everything fancy (custom keys, expiry, view limits) hangs off these two without changing the core shape.
🗝️ Generating the Key
Here’s a part interviewers love poking at: how do we make that short aZ3kP key, and make sure two pastes never get the same one? When two things accidentally share a key, that clash is called a collision, and we have to avoid it.
This is the exact same problem as the short code in a URL shortener, so we lean on the same two tricks.
- Base62 of an id. The database hands each new paste a number that goes up by one each time. We convert that number into a short tag using base62, which writes numbers using 62 symbols (
0-9,a-z,A-Z) instead of 10. Because each id is unique, each key is unique, so there are no collisions to worry about. - Random string plus a check. Generate a random tag like
aZ3kP, then before saving, check if that key already exists. If it does, generate another one. Random keys are nice because they don’t reveal how many pastes you have, but the extra check costs a little time on every write.
Either way the key stays short. A handful of base62 characters covers billions of pastes, so we won’t run out anytime soon.
Already covered this in depth
Key generation is the heart of the URL shortener case study. If base62 or the collision check feels fuzzy, read Design a URL Shortener first. The trick is identical here.
🗄️ Storing the Data
Now, where do we keep all this? Here’s the important call, and it’s different from the URL shortener: we split the paste into two parts.
- The metadata is the small stuff about the paste: its key, when it was made, when it expires, and where the text lives. Metadata just means “data about the data”. This goes in a database, which is great at quick lookups by key.
- The big text body itself does not belong in a normal database row. Instead we put it in object storage, which is a service built to hold large files cheaply, each one fetched by a name. Think of it like a giant locker room where every paste gets its own locker.
So the database row doesn’t hold the text. It holds a pointer to where the text sits in object storage, like a locker number. When someone opens a paste, we look up the metadata, follow the pointer, and grab the text.
Here’s the tiny set of fields the database row holds.
| Field | What it holds |
|---|---|
key | The short key, like aZ3kP (this is what we look up by) |
text_pointer | Where the actual text lives in object storage |
created_at | When the paste was made |
expires_at | When the paste should auto-delete (empty means never) |
Why not just store the text in the database?
Databases are tuned for small, structured rows and quick lookups. Stuffing big text blobs into them makes rows bloated, backups heavy, and queries slow. Object storage is built for exactly this: large blobs, cheap space, fetched by name. Keep the database lean and let object storage carry the weight.
⏳ Expiry
Lots of pastes are throwaway, like a log you only need for an hour. So we let pastes auto-delete after a set time. The clean way to do this is with a TTL.
- TTL stands for Time To Live. It’s how long a piece of data is allowed to stay around before it’s removed.
- When a user sets
expiry: "1d", we store anexpires_attime one day from now in the metadata. - A background cleanup job runs now and then, finds pastes whose
expires_athas passed, and deletes both the metadata row and the text in object storage.
There’s also a faster shortcut for the cleanup part:
- Many databases and object stores support TTL natively, meaning you just tag the item with an expiry and the system deletes it for you.
- And if someone opens a paste that’s already past its
expires_at, we treat it as gone and return a “not found” instead of the text. So even before the cleanup job runs, an expired paste never leaks.
Check expiry on read too
Don’t rely only on the cleanup job to enforce expiry. Always check expires_at when someone reads a paste. That way a paste behaves as expired the moment its time is up, even if the cleanup hasn’t gotten to it yet.
🏗️ High-Level Design
Okay, let’s put the pieces together. When you zoom out, the whole system is a few boxes talking to each other.
Let’s trace what happens for each of our two jobs.
Creating a paste (a write):
- The client sends
POST /pastewith the text and optional expiry. - An app server saves the text in object storage and gets back a pointer.
- It generates a key, then saves the metadata (key, pointer,
expires_at) in the database. - It sends the key and link back to the client.
Reading a paste (a read):
- The client opens
GET /aZ3kP. - The app server checks the cache first, that fast store where we keep the busiest pastes. We’ll cover this next.
- On a miss, it looks up the metadata in the database, follows the pointer to fetch the text from object storage, then saves it in the cache for next time.
- It checks
expires_at, and if the paste is still alive, returns the text.
That’s the full loop. Notice the cache sitting right in the read path, ready to make those opens fast.
⚡ Making Reads Fast
Remember our big fact? The system is read-heavy. Lots of people opening pastes, far fewer creating them. So we pour our effort into making reads quick. Here’s how.
- Most opens go to a small set of popular pastes, like one snippet shared in a big group chat. So we keep those hot pastes in a cache, a small super-fast store (usually in memory) that holds the data people ask for most.
- A common choice is Redis, an in-memory store. “In-memory” means it keeps data in fast RAM instead of slower disk, so a cache hit skips both the database and object storage entirely.
- We can also put a CDN in front, which stands for Content Delivery Network, a set of servers spread around the world. Since a paste’s text doesn’t change once saved, the CDN can keep copies close to users so the request doesn’t even travel far.
Why caching fits a paste so well
A paste is written once and never edited. That makes it perfect to cache, because cached copies can never go stale. A small cache of the hottest pastes can serve a big chunk of all reads without ever touching the database or object storage.
📈 Scaling It
Now imagine this thing gets huge, millions of pastes and reads. One server and one database won’t cut it. Here’s how we grow it.
- Stateless app servers behind a load balancer. “Stateless” means a server keeps no memory of past requests, so any server can handle any request. A load balancer is the traffic cop that spreads incoming requests across all the servers. Because the servers are stateless, we just add more of them when traffic grows.
- Read replicas. A read replica is an extra copy of the database that only handles reads. Since we’re read-heavy, we send all the metadata lookups to replicas and keep the main database free for writes.
- Cache aggressively. As we said, Redis up front soaks up most of the read traffic before it ever reaches the database.
- Shard by key. Sharding means splitting one giant database into smaller pieces, called shards, so no single machine holds everything. We can shard by
key, so each shard owns a slice of the pastes and handles its own lookups.
Object storage helps a lot here too. It’s built to scale on its own and grow basically without limit, so the heavy text never crowds our database.
🧰 Tech Choices
Part of system design is not just naming pieces, it’s saying why you picked each one. Here are the main technology decisions for this system and the reason behind each.
| Decision | Choice | Why |
|---|---|---|
| Make the short key | Base62 of a counter / random id | Short, unique keys for each paste. |
| Store the paste text | Object storage + metadata in DB | Large text stored cheaply; small facts in a quick store. |
| Make reads fast | Cache + CDN | Popular pastes served from memory and from nearby. |
| Auto-delete old pastes | TTL (expiry) | Pastes disappear at their set time without manual cleanup. |
⚠️ Common Mistakes and Misconceptions
A few things trip people up on this one. Let’s clear them out.
- “Just store the huge text blob in a normal database row.” That bloats rows, slows queries, and makes backups painful. Put the text in object storage and keep only a pointer in the database.
- “Pastes can live forever, no need for expiry.” Without expiry, junk piles up forever and you pay to store text nobody wants. A TTL keeps things tidy and is often a real requirement.
- “It’s just save and fetch, caching doesn’t matter.” It matters a lot. The service is read-heavy and pastes never change, so a cache and CDN carry most of the load. Ignoring that means overloading your database for no reason.
- “Random keys can’t collide.” They can. If you use random keys, you must check for an existing key before saving. Base62 of an id sidesteps the check entirely.
- “Object storage is just a fancy database.” No. It’s built for large blobs fetched by name, not for queries, joins, or quick lookups by many fields. That’s why we pair it with a database for the metadata.
🛠️ Design Challenge
Try extending the design yourself. Think each one through first, then open the answer to see a full breakdown.
Syntax highlighting. Let a paste be marked as Python or JavaScript so it shows up colored. Where do you store the language, and where does highlighting happen?
Private pastes. Let a paste be visible only to people with a secret. How do you keep it out of search and stop key guessing?
View limits. Let a paste self-destruct after, say, 10 views. Where do you keep the count, and how do you stop the cache serving it after the limit?
🧩 What You’ve Learned
You can now design a Pastebin from scratch and talk through it clearly. Here’s what you picked up.
- ✅ The core job: save a blob of text, and fetch it back by a short key.
- ✅ Functional vs non-functional requirements, and why you gather them first.
- ✅ The system is read-heavy, and the text can be large, which shapes every choice.
- ✅ A two-endpoint API:
POST /pasteto create,GET /keyto read. - ✅ Generating short keys with base62 of an id, or random plus a collision check.
- ✅ Splitting storage: metadata and a pointer in the database, the big text in object storage.
- ✅ Auto-deleting pastes with a TTL, checked both by a cleanup job and on read.
- ✅ Making reads fast with a cache and CDN, since pastes never change.
- ✅ Scaling with stateless servers, read replicas, caching, and sharding by key.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
Where does the paste text live, and what does the database row hold?
Why: Large text goes in object storage while the database keeps small metadata and a pointer, so rows stay small and lookups stay fast.
- 2
Why is a Pastebin treated as a read-heavy system?
Why: One paste is saved once and then viewed many times, so reads outnumber writes and get the optimization effort.
- 3
How does paste expiry work in this design?
Why: Storing expires_at with a TTL plus checking it on every read means a paste behaves as expired the moment its time is up.
- 4
Why is caching especially safe and effective for pastes?
Why: A paste never changes after it's saved, so a cached copy is always correct, letting a small cache serve most reads.
🚀 What’s Next?
This case study reuses ideas from two earlier topics. Go deeper on them next.
- Design a URL Shortener covers the key-generation trick in full, the exact one we reused here.
- Object Storage Explained digs into why big blobs belong in object storage and not in a database row.
Once you’re comfortable with those, come back and try the design challenge again. You’ll see the whole system click into place.