After designing a URL Shortener, I wanted to work on another system-design problem that would expose me to different concepts.
So I picked Pastebin.
At first, I thought the problem would be simple:
Store some text and return a URL.
But once I started thinking about scale, expiration, large content, caching, database selection, and failure scenarios, there was much more to it.
More importantly, I wanted to change how I approached system design.
Instead of starting with:
"Which technologies should I use?"
I started with:
"What problem am I actually trying to solve?"
This article walks through my design and, more importantly, the reasoning behind it.
1. What are we building?
The basic idea is simple.
A user submits some text:
Hello, this is my paste!
The system generates a unique URL:
https://pastebin.com/abc123
Anyone with the URL can retrieve the paste.
For this design, I considered these requirements:
Functional requirements
- Create a paste
- Retrieve a paste
- Delete a paste
- Set an expiration time
- No authentication
- No editing after creation
Since there is no authentication, deletion is handled using a delete token generated when the paste is created.
2. What happens when a paste is created?
The basic flow looks like this:
Client
|
v
POST /pastes
|
v
API Server
|
+---- Generate Snowflake ID
|
+---- Convert ID to Base62
|
+---- Generate Delete Token
|
+---- Check content size
|
+-----+-----+
| |
<= 1 MB > 1 MB
| |
v v
MongoDB S3
| |
+-----+-----+
|
v
Return paste URL
The interesting part here is the storage decision.
3. MongoDB or S3?
This was one of the biggest decisions I had to think about.
Initially, I considered storing everything in MongoDB.
But then I asked:
What happens when paste contents become large?
A database is not necessarily the best place for every type of data.
So I separated metadata from large objects.
MongoDB
MongoDB stores:
pasteId
deleteTokenHash
storageType
objectKey
createdAt
expiresAt
content
S3
Amazon S3 stores the actual content when the paste is large.
My application-level rule is:
Content <= 1 MB
↓
MongoDB
Content > 1 MB
↓
S3
So MongoDB remains responsible for application metadata, while S3 handles larger paste content.
4. Why MongoDB?
One of the things I realized while working on Pastebin was that database selection should start from requirements and access patterns.
Pastebin doesn't have complex relationships between entities.
A paste is mostly an independent document:
Paste
├── pasteId
├── deleteToken
├── createdAt
├── expiresAt
└── content / objectKey
So a document database such as MongoDB is a reasonable choice for this design.
The important lesson for me wasn't:
"Pastebin uses MongoDB."
It was:
Understand the data and access patterns first, then choose the database.
5. Generating the Paste ID
I reused an idea from my URL Shortener design:
Snowflake ID + Base62
The process is:
Snowflake
↓
Unique numeric ID
↓
Base62 encoding
↓
Short paste ID
For example:
123456789012345
↓
Base62
↓
8dK92x
The final URL becomes:
https://pastebin.com/8dK92x
Here, Snowflake solves the distributed ID generation problem, while Base62 makes the representation shorter.
6. Database Schema
The final MongoDB document looks approximately like this:
{
pasteId: "8dK92x",
deleteTokenHash: "...",
storageType: "DB",
content: "Hello, World!",
objectKey: null,
createdAt: "...",
expiresAt: "..."
}
For a large paste:
{
pasteId: "7xP92a",
deleteTokenHash: "...",
storageType: "S3",
content: null,
objectKey: "pastes/7xP92a",
createdAt: "...",
expiresAt: "..."
}
This gives us a clear separation:
Small content
→ MongoDB
Large content
→ S3
7. API Design
The APIs are intentionally simple.
Create paste
POST /pastes
Request:
{
"content": "Hello World",
"expiration": "1d"
}
Response:
{
"pasteId": "8dK92x",
"url": "https://pastebin.com/8dK92x",
"deleteToken": "..."
}
Retrieve paste
GET /pastes/:pasteId
The server checks MongoDB first.
If:
storageType = DB
return the content directly.
If:
storageType = S3
use objectKey to retrieve the content from S3.
Delete paste
DELETE /pastes/:pasteId
The delete token can be provided through a request header:
X-Delete-Token: <token>
The server verifies the token before deleting the paste.
8. What happens when a paste expires?
This was another interesting part of the design.
Suppose:
expiresAt = 10:00 AM
At 10:05 AM, somebody requests the paste.
We shouldn't depend entirely on a background cleanup process.
The API checks:
expiresAt < currentTime
If true:
Return 404 / Expired
The paste is therefore logically expired immediately.
But the physical data may still exist temporarily in:
- MongoDB
- S3
- Redis
- CDN
That's where background workers come in.
9. Background Workers
A background worker periodically finds expired pastes.
Background Worker
|
Find expired pastes
|
+---------+---------+
| | |
v v v
MongoDB S3 Cache
delete delete invalidate
For example:
Paste expires
↓
Worker detects expiration
↓
Delete MongoDB metadata
↓
Delete S3 object if present
↓
Invalidate cached data
This taught me an important distinction:
Expiration and deletion are not necessarily the same operation.
The application can immediately treat something as expired while asynchronous processes handle physical cleanup.
10. Caching
Pastebin is naturally read-heavy.
Suppose we have:
10M writes/day
100M reads/day
Repeatedly querying the database for the same popular paste would create unnecessary load.
So I introduced caching.
L1 Cache
Each API server has a local in-memory cache.
API Server
↓
L1 Cache
L2 Cache
If L1 misses:
L1 Cache
↓ miss
Redis
↓ miss
MongoDB / S3
This gives us:
L1 → fastest
L2 → shared cache
DB/S3 → source of truth
11. What if Redis goes down?
This was one of the questions I found particularly useful while designing the system.
If Redis is unavailable:
L1
↓ miss
Redis
↓ unavailable
MongoDB
The system becomes slower, but the data isn't lost.
This reinforced an important principle:
A cache should improve performance, not become the only source of truth.
Redis can also be deployed with high availability when the workload requires it.
12. CDN
What if a paste suddenly becomes extremely popular?
For example:
1 paste
↓
millions of requests
Sending every request to our API servers would be wasteful.
A CDN can cache frequently accessed content closer to users.
User
↓
CDN
↓ cache hit
Paste Content
This allows the CDN to absorb a large portion of read traffic before requests reach our backend.
13. Load Balancer
We don't want a single API server handling everything.
Instead:
Load Balancer
/ | \
/ | \
API 1 API 2 API 3
The API servers are stateless, so requests can be distributed across them.
If traffic increases:
3 servers
↓
10 servers
↓
50 servers
This gives us horizontal scalability.
14. Database Scaling
As the system grows, a single MongoDB instance may eventually become a bottleneck.
Replication
We can have:
Primary
/ \
/ \
Replica 1 Replica 2
Replication improves availability and can also support read scaling depending on consistency requirements.
Sharding
If the dataset or traffic becomes too large:
MongoDB
|
+--------+--------+
| | |
Shard 1 Shard 2 Shard 3
We can distribute data across multiple shards using an appropriate shard key.
15. Rate Limiting
Pastebin is a public service, so we also need to protect it from abuse.
For example:
100 requests / minute / IP
A rate limiter can sit before the application logic:
User
↓
Rate Limiter
↓
Load Balancer
↓
API Servers
Redis can be used to maintain distributed rate-limit counters.
16. Final Architecture
Putting everything together:
USERS
|
v
CDN
|
Rate Limiter
|
v
Load Balancer
|
+-------------+-------------+
| | |
v v v
API #1 API #2 API #3
| | |
+-------------+-------------+
|
v
L1 Cache
|
Cache Miss
|
v
Redis (L2)
|
Cache Miss
|
v
+-------------+
| MongoDB |
| |
| Metadata |
| Small Data |
+------+------+
|
Large Content
|
v
S3
Background Workers
|
+-----------+-----------+
| | |
v v v
MongoDB S3 Cache
Cleanup Cleanup Invalidation
17. What I Learned
The biggest lesson from this project wasn't MongoDB, Redis, S3, or CDN.
It was how to think about system design.
Initially, I was approaching system design like this:
"I know Redis.
I know MongoDB.
I know CDN.
Let me add them to the architecture."
Now I'm trying to approach it like this:
Requirements
↓
Traffic & Workload
↓
Data & Access Patterns
↓
Identify Bottlenecks
↓
Consider Alternatives
↓
Choose Components
↓
Understand Trade-offs
↓
Design Architecture
For example:
Why S3?
Not simply because:
"S3 is popular."
But because:
"Large paste content is better separated from frequently queried metadata, and object storage is designed for storing large objects."
Why Redis?
Not simply because:
"Every scalable system needs Redis."
But because:
"Pastebin is read-heavy, and caching frequently accessed pastes can reduce database load and latency."
Why MongoDB?
Not simply because:
"It's NoSQL."
But because:
"Our data is document-oriented and doesn't require complex relational relationships for the core use case."
Conclusion
Designing Pastebin looked simple at first.
But it introduced several system-design concepts that I hadn't thought deeply about before:
- Database selection
- Object storage
- Snowflake IDs
- Base62 encoding
- Caching
- L1/L2 caching
- CDN
- Load balancing
- Rate limiting
- Expiration
- Background workers
- Database replication
- Database sharding
- Failure handling
- Storage trade-offs
More importantly, I'm learning that System Design isn't about memorizing architectures.
It's about being able to explain:
What problem are we solving?
What options do we have?
Why did we choose this one?
What trade-off are we accepting?
That's the skill I'm trying to build — one system at a time. 🚀
Top comments (0)