# Design File Storage

Course: [High-Level Design](https://www.devtutorials.ai/learn/system-design/hld) · Section: HLD Case Studies
Source: https://www.devtutorials.ai/learn/system-design/hld/case-studies/file-storage

> Store QuickBite's menu photos and receipts: object storage for bytes, a metadata database, presigned uploads, CDN downloads and background thumbnails.

Every QuickBite restaurant has menu photos. Every order creates a receipt PDF. Riders upload a photo after delivering an order. Together, this creates millions of files. During dinner time, customers download about 20,000 photos every second.

Files are different from normal database records. A photo may be 5 MB, while a database row may only be a few hundred bytes. People do not search inside image files. They only upload, download or delete them.

The main idea: store file bytes in object storage, store file information in a metadata database, upload directly to storage and serve downloads through a CDN.

## What You Will Learn

In this case study, you will:

1. Write the requirements for file storage.
2. Estimate storage and bandwidth.
3. Separate file content from file metadata.
4. Upload files using presigned URLs.
5. Serve files through a CDN.
6. Generate thumbnails in the background.
7. Build and test a scalable file storage system.

This builds on Lesson 3 (Latency & Distance), Lesson 5 (Caching), Lesson 6 (Databases) and Lesson 9 (Queues & Workers).

## Step 1: Requirements

### What It Must Do (Functional)

- Upload photos and PDFs up to 10 MB.
- Download files using a link.
- Support full-size and thumbnail images.
- Delete files when no longer needed.
- Keep receipts private.
- Store information about each file.

### How Well It Must Do It (Non-Functional)

- Downloads should start within 100 ms.
- Files should survive machine or data center failures.
- Storage should remain affordable.
- Support many downloads at the same time.
- Uploads should stay fast during peak traffic.

## Step 2: Estimate

QuickBite's storage numbers:

| What | Per day | Size each | Total per day |
|---|---|---|---|
| Photos (menus, delivery proof, profiles) | 150,000 | 5 MB + 0.5 MB thumbnails | 825 GB |
| Receipt PDFs | 1,000,000 | 60 KB | 60 GB |
| **All new files** | | | **885 GB a day, about 26.6 TB a month** |

What to notice:

- Storage grows by tens of terabytes every month.
- A normal database is not a good place for this.
- One server is not enough.
- We need distributed file storage.

Downloads at dinner time: 20,000 photo views a second × 300 KB each (phones get a resized photo, not the 5 MB original) × 8 bits = **48 Gbit/s** of outgoing bandwidth.

What to notice:

- 48 Gbit/s is very large.
- Application servers cannot handle all downloads.
- We need a CDN.

## Step 3: Bytes and Details Live Apart

Split every file into two parts:

- File bytes go to object storage.
- File details go to the metadata database.

Object storage stores:

- Menu photos
- Receipt PDFs
- Delivery photos
- Profile pictures

The metadata database stores:

- File ID
- Storage key
- Owner
- File type
- Upload time
- Thumbnail information

Do not store large files inside a database. Backups become larger, replication becomes slower and costs increase.

## Step 4: Upload Straight to Storage

A simple design looks like this:

1. Phone uploads file to app server.
2. App server receives file.
3. App server uploads file to storage.

This works at first but does not scale well.

A better solution is a presigned URL.

1. Phone requests an upload URL.
2. App server checks permissions.
3. App server generates a presigned URL.
4. Phone uploads directly to storage.
5. Metadata is saved after upload.

The URL works for one file, expires after a short time (here 5 minutes), and lets the phone upload straight to storage. Step through one upload:

Here is the same idea in code:

```js runnable title="A presigned link only works for a short time"
const issuedAt = 1_000;      // seconds
const validFor = 300;        // 5 minutes

function linkStillValid(now) {
  return now <= issuedAt + validFor;
}

for (const now of [1_060, 1_299, 1_301]) {
  console.log(`at ${now} s: ${linkStillValid(now) ? "upload allowed" : "link expired"}`);
}
```

Expected output:

```text
at 1060 s: upload allowed
at 1299 s: upload allowed
at 1301 s: link expired
```

What to notice:

- App servers do not handle large file uploads.
- Uploads become faster.
- Storage handles the file transfer.

After upload:

1. Save metadata.
2. Add a thumbnail job to a queue.
3. Workers generate thumbnails.

## Step 5: Download From a CDN

A CDN stores copies of files close to users.

A customer in Bengaluru downloads from a nearby server instead of a distant storage region.

Menu photos are ideal for a CDN because:

- Many users request the same images.
- Images rarely change.
- Cache hit rates are high.

The CDN's hit ratio decides how much traffic still reaches object storage. Step through three hit ratios:

Private files can still use a CDN through signed URLs.

If a photo changes, upload it with a new filename instead of replacing the old file.

## Step 6: Thumbnails in the Background

Customers do not need the full-size image all the time.

Instead, create:

- Small thumbnail
- Medium thumbnail
- Original image

When an upload finishes, a job goes on a queue and workers make the thumbnails:

Generate thumbnails in background workers. Do not generate them during the upload request.

## Step 7: Build It on the Canvas and Test It

The starting design contains:

- Load balancer
- 4 photo API servers
- Object storage
- Metadata database

There is no CDN.

| Check | Passes when |
|---|---|
| Peak load | p99 ≤ 100 ms and thumbnails ready within 2 seconds |
| Machine failure | Errors under 1% and p99 under 250 ms |
| Cost | Under $1,500/month |
| Requirements | Queue-based thumbnail generation |

Run the starting design first.

Then:

- Add a CDN.
- Switch uploads to presigned URLs.
- Add a queue.
- Add thumbnail workers.

What you will probably find:

- Without a CDN, downloads overload the system.
- Uploading through app servers creates bottlenecks.
- Presigned URLs fix upload bottlenecks.
- One thumbnail worker is a single point of failure.

A design that passes:

- 2 load balancers
- 2 photo API servers
- Object storage
- CDN
- 2 database copies
- Queue
- 2 thumbnail workers

## Common Mistakes

| Mistake | Wrong | Correct |
|---|---|---|
| Files in database | Store images as BLOBs | Store bytes in object storage |
| Proxy uploads | Upload through app servers | Upload directly to storage |
| No CDN | Serve all downloads from storage | Use a CDN |
| Overwriting files | Reuse the same filename | Use new filenames |
| Inline thumbnails | Resize during upload | Use queues and workers |

## Practice

## What to Remember

- Store file bytes in object storage.
- Store metadata in a database.
- Use presigned URLs for uploads.
- Use a CDN for downloads.
- Generate thumbnails with queues and workers.
- Give updated files new names.
- Bandwidth is often the biggest scaling challenge.

## Further Reading

These are the sources this lesson was checked against:

- [Amazon S3: Uploading objects with presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html): how short-lived upload links work.
- [Cloudflare: What is a CDN?](https://www.cloudflare.com/learning/cdn/what-is-a-cdn/): edge servers, cache hits and why distance matters.
- [Google Cloud Storage: Storage classes](https://cloud.google.com/storage/docs/storage-classes): how object storage trades price for how often files are read.

Next: **Search autocomplete**, where QuickBite suggests dishes and restaurants as you type.
