Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

System Design (High-Level Design)  Module 22 – Case studies: links, files and media

Design a paste service like Pastebin

Design a paste service: text in object storage, metadata in a table, expiry enforced on every read, unguessable links and takedowns that reach every copy.

  • Beginner
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free intro, Pro: 4 of 25 minutes free
Free: About 700 reads a second, and storage set by the pastes that never expire. With a pass: the other 13 sections and 3 interview answers, about 21 minutes.

What you will learn

  • Store small text blobs with expiry and access control
  • Separate metadata from content storage
  • Handle abuse reports and takedown requests
  • Estimate storage from a mix of expiry choices

Before you start

What you’ll learn · Before you start

What you will learn

  • Store small text blobs with expiry and access control
  • Separate metadata from content storage
  • Handle abuse reports and takedown requests
  • Estimate storage from a mix of expiry choices

Before you start

On this page

Not affiliated

Based on public information about Pastebin. MySmartCoPilot is not affiliated with Pastebin, and Pastebin’s real architecture may differ from what this lesson describes.

A paste service stores a block of text, such as a log, a code snippet or a configuration file, and returns a link that shows it. The design splits every paste in two: the text goes to an object store as one object, and a small row in a database records who owns it, who may read it and when it expires. Every read checks that row before it returns any text, which is what keeps expired and removed pastes from coming back. Unlisted links get long random ids, public pastes are served by a CDN, and an abuse report can end in a takedown that has to reach every copy within the hours a law allows. Next to a URL shortener the payload is bigger, from a few bytes to a megabyte, and it has a lifetime.

About 700 reads a second, and storage set by the pastes that never expire

Ask these before drawing anything, and note the answer you assume:

  • How large can a paste be? Up to 1 MB of text.
  • When does a paste expire? The creator picks: after 10 minutes, an hour, a day, a week, a month, a year, never, or right after its first reading.
  • Who can read it? Anyone for a public paste; anyone with the link for an unlisted one; only its signed-in owner for a private one.
  • Can a paste change? No. An edit makes a new paste.

Pastebin’s own API offers the same kind of choices: expiry from 10 minutes to a year or never, and the three levels public, unlisted and private (Pastebin API documentation).

The functional requirements: create a paste, read it by id, list your own pastes, delete one, report one. The non-functional ones: reads return in tens of milliseconds and stay available when creates are failing; a paste is never readable after it expires or is removed; nobody can find an unlisted paste without its link; and a takedown reaches every copy within hours.

The paste service's estimate sheet Python · estimate/paste_sheet.py
# The estimate sheet of a paste service (lesson system-design-hld/design-pastebin). Pastes expire, so the number kept
# is set by how long each one lives: Little's law, kept = created per day x days kept, for every expiry choice. The
# pastes that never expire are the exception: they pile up year after year. Sizes are decimal (1 TB = 10**12 bytes).
DAY = 86_400

NEW_PER_DAY = 2_000_000
READS_PER_DAY = 20_000_000  # ten reads per paste on average; a few popular pastes take most of them
PEAK = 3
PASTE_BYTES = 8_000  # the average paste; the service caps a paste at 1 MB
META_BYTES = 300  # one metadata row with its indexes
META_COPIES = 3
YEARS = 5
CDN_SHARE = 0.6  # share of reads the CDN serves: public and unlisted pastes, never private ones

# Share of new pastes for each expiry choice, and how long each lives, in days (None: never expires).
EXPIRY = {"10 minutes": (0.05, 10 / 1440), "1 hour": (0.10, 1 / 24), "1 day": (0.25, 1), "1 week": (0.20, 7),
          "1 month": (0.15, 30), "1 year": (0.10, 365), "never": (0.15, None)}

assert abs(sum(share for share, _ in EXPIRY.values()) - 1) < 1e-9

kept = {}
for name, (share, days) in EXPIRY.items():
    lifetime = YEARS * 365 if days is None else min(days, YEARS * 365)  # nothing is older than the service
    kept[name] = NEW_PER_DAY * share * lifetime
total = sum(kept.values())

print("Paste service: the estimate sheet")
print(f"  Creates    {NEW_PER_DAY / DAY:,.0f}/s on average, {NEW_PER_DAY / DAY * PEAK:,.0f}/s at the peak")
print(f"  Reads      {READS_PER_DAY / DAY:,.0f}/s on average, {READS_PER_DAY / DAY * PEAK:,.0f}/s at the peak,")
print(f"             {READS_PER_DAY / DAY * PEAK * (1 - CDN_SHARE):,.0f}/s of them reach the servers")
print(f"  Deletes    {NEW_PER_DAY * (1 - EXPIRY['never'][0]) / DAY:,.0f}/s on average: every paste that expires")
print(f"  Kept after {YEARS} years, by expiry choice:")
for name, n in kept.items():
    print(f"    {name:<11}{n:>13,.0f}  {n / total:>6.1%}")
print(f"    {'all':<11}{total:>13,.0f}")
print(f"  Content    {total * PASTE_BYTES / 10**12:.1f} TB in the object store")
print(f"  Metadata   {total * META_BYTES / 10**9:,.0f} GB, {total * META_BYTES * META_COPIES / 10**9:,.0f} GB with {META_COPIES} copies")
print(f"  Bandwidth  {READS_PER_DAY / DAY * PEAK * PASTE_BYTES * 8 / 10**6:.0f} Mbit/s of pastes at the peak")

Output

Paste service: the estimate sheet
  Creates    23/s on average, 69/s at the peak
  Reads      231/s on average, 694/s at the peak,
             278/s of them reach the servers
  Deletes    20/s on average: every paste that expires
  Kept after 5 years, by expiry choice:
    10 minutes           694    0.0%
    1 hour             8,333    0.0%
    1 day            500,000    0.1%
    1 week         2,800,000    0.4%
    1 month        9,000,000    1.4%
    1 year        73,000,000   11.5%
    never        547,500,000   86.5%
    all          632,809,028
  Content    5.1 TB in the object store
  Metadata   190 GB, 570 GB with 3 copies
  Bandwidth  44 Mbit/s of pastes at the peak

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 paste_sheet.py

This is a small system by traffic: 69 creates and 694 reads a second at the peak, and 5.1 TB of text after five years. The interesting numbers are about time. Pastes that expire within a month are less than 2 % of what is kept, while the 15 % of pastes that never expire make up 86.5 % of it after five years. And expiry is steady work, 20 deletions a second, not an occasional cleanup.

Exercise · Easy · Python

Size a paste store from its expiry choices

A paste service lets each paste expire after a chosen time, so how much it stores depends on the mix of choices. Write three functions in footprint.py.

live_pastes(new_per_day, mix, years) returns how many pastes are kept once the service has run for years years (365 days each). mix maps each expiry choice's lifetime in days to its share of new pastes; the key None stands for pastes that never expire. A choice keeps new_per_day * share * lifetime pastes, except that nothing can be older than the service: a lifetime longer than the service's age counts as that age, and so does None.

footprint(live, paste_bytes, meta_bytes, meta_copies) returns a dictionary with two keys: "objects", the bytes of content in the object store (one copy: the store keeps its own redundancy), and "metadata", the bytes of the metadata rows with all their copies.

server_reads(reads_per_day, peak, cdn_share) returns the reads a second at the peak that the CDN does not serve, where cdn_share is the share of all reads it serves.

The sample tests accept each figure within 10 %, as an estimate should.

Starter code · footprint.py

DAY = 86_400


def live_pastes(new_per_day, mix, years):
    """Pastes kept after `years` years; `mix` maps lifetime in days (None: never expires) to a share of new pastes."""
    # Replace this line with your code.
    return 0


def footprint(live, paste_bytes, meta_bytes, meta_copies):
    """Bytes of content in the object store and bytes of metadata with its copies."""
    # Replace this line with your code.
    return {}


def server_reads(reads_per_day, peak, cdn_share):
    """Reads a second at the peak that reach the servers."""
    # Replace this line with your code.
    return 0
The sample tests · test_footprint.py
import math

from footprint import footprint, live_pastes, server_reads

LESSON_MIX = {10 / 1440: 0.05, 1 / 24: 0.10, 1: 0.25, 7: 0.20, 30: 0.15, 365: 0.10, None: 0.15}
SHORT_MIX = {1: 0.5, 30: 0.3, None: 0.2}


def near(actual, expected):
    """Within 10%, as an estimate should be."""
    return math.isclose(actual, expected, rel_tol=0.10)


def test_live_pastes_lesson():
    """keeps each expiry choice for its lifetime, and never-expiring pastes for the service's age"""
    assert near(live_pastes(2_000_000, LESSON_MIX, 5), 632_800_000)


def test_live_pastes_other_mix():
    """works for another mix and period"""
    assert near(live_pastes(1_000_000, SHORT_MIX, 2), 155_500_000)


def test_young_service():
    """counts a lifetime longer than the service's age as that age"""
    assert near(live_pastes(1_000_000, {365: 1.0}, 0.5), 182_500_000)
    assert near(live_pastes(1_000_000, {365: 0.5, None: 0.5}, 0.5), 182_500_000)


def test_footprint():
    """stores one copy of the content and every copy of the metadata"""
    sizes = footprint(632_800_000, 8_000, 300, 3)
    assert near(sizes["objects"], 5.06e12)
    assert near(sizes["metadata"], 5.7e11)


def test_server_reads():
    """leaves the CDN's share of reads out"""
    assert near(server_reads(20_000_000, 3, 0.6), 278)
    assert near(server_reads(50_000_000, 2, 0.9), 116)
A hint

Loop over mix.items(). For each lifetime, use the service's age, years * 365, when the lifetime is None or longer than that age, then add new_per_day * share * lifetime. The reads at the peak are reads_per_day / DAY * peak, and the servers see the part the CDN does not serve, 1 - cdn_share of them.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Checkpoint

Check the estimate

  1. Question 1 of 2 The sheet assumes 20 million reads a day and a busiest hour at 3 times the average. About how many reads a second arrive at the peak?

    Type a number.

    A hint

    A day has 86,400 seconds.

    Show the answer: Question 1 of 2

    Answer: 694 reads a second (anything from 684 to 704 counts)

    20,000,000 ÷ 86,400 ≈ 231 reads a second on average, and three times that is about 694. With 60 % of the reads answered by the CDN, about 278 a second reach the servers: a small system by traffic.

  2. Question 2 of 2 After five years, which pastes make up most of what the service keeps?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The 15 % of new pastes that never expire, which become 86.5 % of everything kept

    What a store holds is how many arrive a day times how long each one stays. Pastes that never expire stay for the whole life of the service, so 15 % of creates become 86.5 % of the 633 million pastes kept after five years.

The first design: the row is read before any text

Reads pass a CDN to API servers that check a metadata row before fetching text from an object store; jobs and takedowns act on both.Browser, app or scriptCDNpublic and unlisted pastes,never past their expiryAPI serverscreate, read, delete, reportMetadata tableid, owner, visibility, expiry,size, stateObject storeone object per pasteBackground jobsexpiry cleanup, abuse scanningAbuse and legal deskreports and takedownsGET /<id>POST /api/pastes,private pastesmisses1. check the row2. then fetch the textmark removedpurge

The paste service's first design

Text description of the diagram

The diagram runs from top to bottom.

  1. A browser, app or script reads a paste with GET /<id> through a CDN, which keeps public and unlisted pastes, but never past their expiry.
  2. Misses at the CDN, every create (POST /api/pastes) and every read of a private paste go to the API servers, which create, read, delete and take reports.
  3. To read a paste, an API server first checks its row in the metadata table (id, owner, visibility, expiry, size and state), and only then fetches the text from the object store, where each paste is one object.
  4. Background jobs work on both stores: they clean up expired pastes and scan new ones for abuse.
  5. The abuse and legal desk handles reports and takedowns: it marks a paste as removed in the metadata table and, shown with a dashed arrow, sends a purge to the CDN.

Follow a read through it. The CDN answers public and unlisted pastes it holds; everything else reaches an API server, which reads the paste’s row first and fetches the text from the object store only if the row allows it. A create writes the text, then the row. Background jobs clean up expired pastes and scan new ones, and the abuse desk can mark a paste removed and purge (CDN): Removing a URL, or every response with a tag, from a CDN's caches before its lifetime runs out. It cannot reach the copies that browsers already hold. More in “CDNs and edge caching” it from the CDN. Step through one paste, from its create to its removal:

Interactive · step by step One paste through the first design Step through a paste being created, read twice and removed, and see which store each step touches.
One paste through the first design, step 10 of 10. Showing Browser, app or script, CDN (no copy of k3Tq9), API servers, Abuse and legal desk, Metadata table (k3Tq9: removed), Object store (holds pastes/k3Tq9), Background jobs. In focus: Background jobs.Browser, app or scriptCDNno copy of k3Tq9API serversAbuse and legal deskMetadata tablek3Tq9: removedObject storeholds pastes/k3Tq9Background jobscleanup and scanningexpired and new pastes

The drawing shows the whole scene; its 10 steps are listed below.

Every step as text
  1. A create. An app sends a log file to the API servers, to expire in one day and be readable by anyone with the link. The paste gets the id k3Tq9.
  2. The text first. The text goes to the object store as one object, named after the paste's id.
  3. Then the row. Then the row, with the owner, the visibility, the expiry and the state.
  4. A read misses the CDN. Someone opens the link. The CDN holds no copy yet, so it passes the read on to the API servers.
  5. The row first. The API server reads the row before anything else. It exists, it is active and its expiry is still ahead, so the text may be served.
  6. Then the text. Only now does it fetch the text from the object store. The CDN may keep the answer for later readers, but never past the paste's expiry.
  7. Answered at the edge. The next reader of the link is answered by the CDN, and no server does any work for that read.
  8. A report. A reader reports the paste. The abuse desk marks it removed in its row, so every read at the servers stops at once.
  9. Purged from the edge. The desk also purges the paste from the CDN, so the copy at the edge stops too.
  10. Background work. Background jobs clean up expired pastes and scan new ones for abuse, away from every read and create.

The questions this design has to answer:

  • Why is the text in an object store and not in the row?
  • How do 10-minute expiries work when storage lifecycle rule: A bucket setting that moves objects to another storage class, or deletes them, once they reach a given age. Each transition is a billed request, and S3 does not move objects smaller than 128 KB by default. More in “Block, file and object storage” count in days?
  • What stops an expired or removed paste from coming back out of a cache?
  • How many random characters keep an unlisted paste from being found?
  • How does a burn-after-reading paste reach exactly one reader?
  • How does a takedown reach every copy within the legal deadline?
  • What changes at ten and a hundred times the traffic?

Checkpoint

Check the first design

  1. Question 1 of 2 A read of a paste reaches an API server. What does the server read first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The paste's row in the metadata table

    The row says whether the paste exists, who may read it and whether it has expired or been removed, so the server reads it first and fetches the text from the object store only if the row allows it.

  2. Question 2 of 2 The abuse desk removes a public paste that many people have already opened. What does the first design do so that readers stop seeing it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It marks the paste removed in its row and purges it from the CDN

    A paste can be served from two places, the servers and the CDN's edges. Marking the row removed closes the first, and a purge closes the second.

Status codes clients and caches act on, and an index for every query Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • Text in an object, a small row beside it, and the text written first
  • Expiry is checked on every read, because cleanup always runs late
  • An unlisted paste is only as safe as its id is long
  • Caches keep a paste no longer than it lives, and removal purges both addresses
  • Abuse is handled at creation, after it and on a legal clock
  • At 10x the rows are partitioned, and at 100x scanning leaves the create path
  • Alternatives considered and rejected
  • The seven questions, answered
  • Level expectations, and a self-mock in 45 minutes
  • Practise: the whole design, one question at a time
  • Interview questions
    • What is the difference between a public, an unlisted and a private paste?
    • Users report that a paste set to expire after 10 minutes could still be opened half an hour later. Where could it have come from, and how do you make expiry hold on every path?
    • Unlisted pastes are protected only by their link. How long must their ids be, and what else keeps people from finding unlisted pastes they were never sent?
    • A government order to remove a public paste arrives at 2 a.m., and the law gives your service only a few hours. Design the path from the order to the paste being gone everywhere, and say what you keep afterwards.
  • Key takeaways

3 diagrams · 3 runnable examples (Python) · 3 interview answers · about 55 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Read a complete sample first: Design a URL shortener with click analytics

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.