System designCore10 min
Object & blob storage
Photos, videos, documents and backups are too big for a database and too important for a server's disk. Object storage keeps them as whole files under keys, in a service built to hold practically unlimited data durably. It is simple to use, and its few rules decide how uploads, serving and costs should be designed.
1 · The problem
Files belong neither on app servers nor in the database
A marketplace for secondhand furniture gets 30,000 new listings a day, each with about 8 photos of 4 MB. That is 30,000 × 8 × 4 MB = 960 GB of photos a day, about 350 TB a year. Where should they go?
Put the photos… | What goes wrong |
|---|---|
On the app server's disk | Another server can't see the file, and a replaced server takes its files with it (Stateless services & sessions) |
In the database | Nearly a terabyte a day swells backups and replicas, and streaming bytes ties up the database's memory and connections |
In a shared file system | Built for files that many machines open and edit in place, which photos never need |
In object storage | Built for exactly this: whole files, any number, read from anywhere over HTTP |
Storage comes in three shapes. Block storage is a raw disk divided into fixed-size blocks; databases and virtual machines run on it. File storage is a shared file system with folders and permissions. Object storage keeps each file as a single object under a unique name, reached through a web API. Azure calls objects blobs; the idea is the same.
2 · The model
An object store is a huge key-value store for whole files
Objects live in buckets. Each object has a key (its name), a value (the bytes), and metadata such as its content type and size. You work with whole objects through HTTP: PUT to store, GET to read, DELETE to remove.
Storing and reading one photo
PUT /listings/8812/photo-1.jpg HTTP/1.1
Host: furniture-photos.storage.example
Content-Type: image/jpeg
Content-Length: 4194304
<4 MB of bytes>
GET /listings/8812/photo-1.jpg HTTP/1.1
Host: furniture-photos.storage.exampleA few rules shape everything that follows:
Rule | What it means |
|---|---|
Flat names | listings/8812/photo-1.jpg is one key; a "folder" is just a shared key prefix |
Whole objects only | A change means uploading a complete replacement; no edits or appends in S3 or Google Cloud Storage (Azure has append blobs) |
Strong reads after writes | In S3, a read that starts after a successful upload, overwrite or delete sees it, and listings include it |
Last writer wins | Two uploads to one key don't lock or conflict; the later one replaces the other, and nothing spans several keys atomically |
3 · Uploads
Let clients upload straight to storage with a short-lived signed URL
If every photo passed through the app servers, they would spend their time copying 960 GB a day of bytes they never look at. Instead, the app hands the browser a signed URL: a link that grants permission for one operation on one key until it expires, signed with the app's credentials. The bytes go straight to the object store:
The app server checks who may upload what and chooses the key; it never touches the bytes. The URL works for that one key, for 15 minutes.
A signed URL is a bearer token: anyone who has it can use it until it expires, so keep lifetimes short (S3 and Google Cloud Storage allow at most 7 days) and scope each one to a single key and method. Downloads of private files work the same way, with a signed GET.
Big files, like a seller's 2 GB video walkthrough, use a multipart upload: the file is split into parts that upload independently, in parallel and in any order, and a failed part is retried alone instead of restarting the whole file. In S3, a single PUT takes up to 5 GB, parts are 5 MiB to 5 GiB, up to 10,000 of them, and multipart is worth it from about 100 MB.
4 · Metadata
Keep the bytes in the object store and the facts in a database
The object store answers one question well: give me the object with this key. The marketplace needs many others: the photos of listing 8812 in order, every listing by a seller, photos still waiting for moderation. Those belong in a database row per photo, holding the key, size, checksum, order and status. Pages query the database, then build URLs from the keys.
Uploads also start work. Object stores can send an event when an object is created, which a worker uses to make thumbnails, strip location data from photos and run moderation:
Processing happens after the upload, from a queue, so a slow resize never holds up the seller (Message queues & pub/sub).
Two loose ends need a rule each. Uploads that start but never finish leave orphan objects and half-done multipart uploads, which a cleanup rule deletes after a day or two. And deleting a listing must delete its objects too, or the bill keeps growing for files nobody can reach.
5 · Serving
Serve objects through a CDN, and name them so they never change
Reading straight from an object store is fine for a back-office tool, but small objects take roughly 100 to 200 ms to start arriving, and every view is another request to the store. Public photos go through a CDN, which keeps copies at edges near buyers (Content delivery networks (CDNs)).
Caching works best when an object's content never changes under its name. Put a version or a hash of the content in the key, photo-1.v2.jpg or 3f9a…c1.jpg, and a new photo gets a new key. The CDN and browsers can then cache each key for a year, and nothing ever needs to be purged.
Key | Cache it for | After the seller replaces the photo |
|---|---|---|
| Minutes, or until purged | Old copies keep being served until they expire or are purged |
| A year | The listing points at a new key; old copies are never asked for again |
Object stores scale request rates per key prefix. S3 handles at least 3,500 writes and 5,500 reads a second per prefix, with no limit on the number of prefixes, and it adds capacity gradually, answering 503 Slow Down while it does. Keys that spread across many prefixes, such as listings/8812/…, spread the load; one hot prefix behaves like any hot partition (Hot partitions & skew).
6 · Durability and cost
Durable is not the same as backed up, and storage only ever grows
S3 is designed for 99.999999999% durability, "eleven nines", by storing every object across at least three Availability Zones: separate data centers, kilometers apart. As a model, that is an annual loss chance of 10⁻¹¹ per object: across 10 million objects, 10,000,000 × 10⁻¹¹ = 0.0001 objects a year, one every 10,000 years.
That protects against hardware. It doesn't protect against your own code: a bug that overwrites every thumbnail, or a script that deletes the wrong prefix, is carried out durably. The defenses are separate features:
- Versioning keeps every previous version of an object, so an overwrite or delete can be undone.
- Object lock forbids deleting or overwriting for a set period, for records that must survive.
- Replication to another region guards against losing a whole region (Disaster recovery & backups).
Cost is mostly storage that grows forever. Storage classes trade price per gigabyte against access:
Class (S3 names) | Access | Catch |
|---|---|---|
Standard | Milliseconds | Highest storage price |
Standard-Infrequent Access | Milliseconds | Retrieval fee; billed for at least 30 days and 128 KB |
Glacier Instant Retrieval | Milliseconds | Higher retrieval fee; at least 90 days |
Glacier Flexible Retrieval | Minutes to hours, after a restore | At least 90 days |
Glacier Deep Archive | Hours, after a restore | At least 180 days |
Lifecycle rules apply these automatically by age: move photos tagged as sold to Infrequent Access after 90 days, and delete them two years later. The deletion rule is the one that changes the curve:
- Keep everything
- Delete 24 months after the sale
960 GB a day ≈ 29 TB a month. Kept forever, storage passes 1,400 TB in four years; with a 24-month rule it levels off at about 700 TB, assuming items sell soon after listing.
7 · In practice
A checklist for files in a system design
When a design involves user files, the same handful of decisions come up every time:
Decision | Usual answer |
|---|---|
Where the bytes live | Object storage, never app servers or database rows |
How uploads arrive | Directly from the client with a short-lived signed URL; multipart for big files |
Where the facts live | A database row per object, holding the key and status |
How processing starts | An upload event into a queue, consumed by workers |
How files are served | Through a CDN, with keys that change when content changes |
How mistakes are undone | Versioning, plus replication for whole-region loss |
How cost is controlled | Lifecycle rules to cheaper classes, and deletion |
Also:
- Keep buckets private and grant access with signed URLs or the CDN, never by making a whole bucket public.
- Check what you received: verify size and checksum, and the real content type, before marking an upload ready.
- Don't build a file system on top: renames, appends and many small edits fight the whole-object model; use a database or file storage for those.
Check yourself
5 questions
Takeaways
Remember this
- Object storage keeps whole files under keys in a flat namespace, read and written over HTTP; objects are replaced, never edited.
- Clients upload directly with short-lived signed URLs, and large files go up in parallel parts.
- The bytes live in the object store; the facts you query live in a database row per object.
- Serve public objects through a CDN, with keys that change whenever the content does.
- Eleven nines of durability doesn't undo your own mistakes; use versioning, replication and lifecycle rules.