Design an object storage service
Design the service that stores files for everyone else: Amazon S3, Google Cloud Storage, Azure Blob Storage, or an in-house equivalent. The interface is just put, get, delete and list. Behind it are hundreds of petabytes on disks that fail every day, and a promise that nothing written is ever lost.
1. The prompt
"Design a service like Amazon S3: users create buckets and store objects of any size by key, read them back, list them and delete them."
What makes it hard:
- Durability at scale: with a million disks, several fail every day; data must survive all of it.
- Two very different problems: billions of small metadata records, and petabytes of bytes.
- Cost: storing three full copies of everything is the obvious answer, and the expensive one.
2. Frame the problem
How much data, and how many objects?
About 100 PB today, growing 50% a year, in roughly 100 billion objects, from a few bytes to terabytes each.
Billions of keys means metadata needs its own sharded store; petabytes means the byte layout and redundancy scheme decide the cost.
Traffic?
About 1 million reads and 100,000 writes a second at peak, mostly small objects.
Request rate stresses metadata; bandwidth stresses storage nodes.
Consistency?
A read after a successful write returns the new object; a list after a write includes it.
Strong read-after-write consistency, which S3 has offered since 2020. The metadata commit becomes the single point that decides "written".
Durability?
Losing an acknowledged object should be essentially impossible, even with whole-zone failures.
Redundancy spread across failure domains, plus constant checking and repair.
Large objects?
Up to 5 TB; uploads must survive network blips.
Multipart upload: parts uploaded independently, in parallel, retried alone, then committed together.
Versioning and lifecycle?
Optional versions per bucket; rules to move old data to cheaper tiers or delete it.
Old versions and cold tiers change the storage layout and the cost model.
Access control?
Per bucket and object policies; temporary signed URLs for browsers.
Every request is authenticated before metadata or data is touched.
3. What it must do
- A user can put an object (any size) into a bucket under a key, and get it back.
- A user can list objects in a bucket by key prefix, page by page.
- A user can delete objects, and optionally keep old versions.
Not now: a file-system interface (rename, append, directories), search by content, cross-region replication (a follow-up), billing.
4. How well it must do it
| Target | Why this number |
|---|---|
| Durability: no acknowledged object lost even if a whole zone and some disks elsewhere fail data safety | It's the one promise customers can't check themselves. |
| Availability: 99.99% for reads and writes surviving failures | Other systems depend on it; a zone outage must not stop it. |
| Consistency: strong read-after-write for objects and listings consistency | Clients shouldn't need retries or delays to see their own writes. |
| Latency: first byte of a small object within ~100 ms at p99 speed | Web pages and apps read from it directly. |
| Cost: raw capacity well under 2× the stored data cost | At 100 PB, every 0.1× of overhead is 10 PB of disks. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Metadata | 100 billion objects × ~500 bytes ≈ 50 TB | Too big for one database → so metadata is a separate, sharded, replicated store |
| Redundancy | 3 copies of 100 PB = 300 PB raw; erasure coding with 6 data + 3 parity pieces = 150 PB | Erasure coding saves ~150 PB → so we erasure-code, and keep full copies only where speed matters |
| Disk failures | 1 million drives × ~1% annual failure ≈ 27 a day | Failure is constant → so repair is automatic and continuous, not an incident |
| Small objects | Half the objects are under 100 KB; a file per object means billions of files | File systems choke → so storage nodes pack many objects into large append-only volume files |
Illustrative numbers. Skipped: bucket count (small), and per-user numbers.
6. The nouns
- Bucket: a namespace with an owner, policies and settings.
- Object: key, version, size, checksum, user metadata, and where its bytes live.
- Chunk (or stripe): a piece of an object's bytes, erasure-coded into fragments on different storage nodes.
- Volume: a large append-only file on a storage node that holds many fragments.
bucket name, owner, region, versioning, policy, lifecycle_rules object (bucket, key, version) → size, etag/checksum, content_type, created_at, state, chunks[] chunk chunk_id, placement_group, fragments[(node, volume, offset, length)], ec_scheme (6+3) volume node, volume_id, size, sealed? (fragments appended, never edited)
7. The contract
PUT /{bucket}/{key} body = bytes → 200 {etag, version}
GET /{bucket}/{key}?version=… Range: bytes=… → bytes
DELETE /{bucket}/{key} → 204
GET /{bucket}?prefix=photos/&continuation-token=…&max-keys=1000
→ {keys[], next_token}
POST /{bucket}/{key}?uploads → {upload_id} (multipart)
PUT /{bucket}/{key}?uploadId=…&partNumber=n body → {etag}
POST /{bucket}/{key}?uploadId=… {parts[(n, etag)]} → complete
The caller is identified by a signature on each request (or a presigned URL that grants one operation for a limited time); never by a field in the body. Listing is paged with a continuation token, ordered by key.
Data flow (put): front end authenticates → splits into chunks → erasure-codes each chunk → writes fragments to storage nodes in different zones → all needed fragments durable → commit metadata (the moment the object exists) → 200.
8. First working version
Stateless front ends, a metadata service, and storage nodes. The separation of metadata from bytes is the first decision to say out loud.
8.1 Putting an object
The bytes are written first; the metadata commit comes last. If the front end dies halfway, the fragments are orphans with no metadata pointing at them, and a garbage collector removes them later. Readers never see half an object.
8.2 Getting an object
Metadata says where the fragments are; any 6 of the 9 rebuild the chunk. In practice the front end reads the 6 data fragments directly (no decoding needed) and only reads parity when a node is slow or down.
8.3 Listing
Metadata is partitioned by bucket and key range, not by hash, so a prefix listing reads a contiguous range from one or a few shards, in order.
9. Make it hold up
9.1 Redundancy: copies or erasure coding durability · cost
| Approach | Why | |
|---|---|---|
| ✗ Bad | One copy on RAID | A machine or zone loss loses data; RAID rebuilds of huge disks take days. |
| ~ Good | Three full copies in three zones | Simple, fast reads, survives a zone. Costs 3× raw storage. |
| ✓ Great | Erasure coding: split each chunk into k data fragments, compute m parity fragments (Reed-Solomon), place the k+m on different nodes and zones; any k rebuild the chunk. 6+3 costs 1.5× and survives any 3 losses | About half the raw storage of 3 copies for similar or better durability. Costs: CPU to encode, more nodes per read, slower repair. Small or hot objects can still use copies. |
Placement matters as much as the code: fragments of one chunk go to different disks, nodes, racks and zones, so one failure takes at most a fragment or two from any chunk. Some systems use local reconstruction codes (Azure's LRC) so a single lost fragment is rebuilt from a few neighbours instead of k.
9.2 Disks die every day durability
- Scrubbing: every fragment is re-read and checked against its checksum on a schedule, catching silent corruption (bit rot) before a second failure makes it matter.
- Repair: when a disk or node is lost, every chunk with a fragment there is rebuilt from the survivors onto other nodes. Prioritise chunks that have lost the most fragments.
- Spread repair across many nodes so a 20 TB disk is rebuilt in hours by hundreds of peers, not days by one.
- Durability is the race between failure rate and repair speed: faster repair means fewer chunks ever reach their last k fragments.
9.3 Billions of small objects latency · cost
Storing each object as its own file on a storage node means billions of files, file-system metadata lookups on every read, and wasted blocks. Instead, nodes append fragments to large volume files (gigabytes each) and keep an in-memory index of (fragment → volume, offset, length): one disk seek per read. Facebook's Haystack paper described the same idea for photos. Deleted fragments leave holes, reclaimed by compacting volumes in the background.
9.4 Metadata for 100 billion keys consistency · latency
| Approach | Why | |
|---|---|---|
| ✗ Bad | One relational database | 50 TB and a million lookups a second don't fit one machine. |
| ~ Good | Shard by hash of (bucket, key) | Even load, but listing a prefix must ask every shard. |
| ✓ Great | Range-partitioned by (bucket, key), each range replicated with consensus; split hot or large ranges automatically | Ordered listings from one range; strongly consistent writes; hot prefixes split as they grow. Costs: careful split and merge machinery. |
The metadata write is the linearization point: once it commits, every reader sees the new version, which is how read-after-write consistency is achieved. Caches in front of metadata must be invalidated or versioned for this to hold.
9.5 Huge uploads availability
A 5 TB upload over one connection would almost certainly fail somewhere. Multipart upload: the client starts an upload, sends parts (say 100 MB each) in parallel, retries any part alone, then completes the upload with the list of parts. Only the completion writes the object's metadata; abandoned uploads are cleaned up by a lifecycle rule. Reads support byte ranges, so large objects are also downloaded in parallel.
9.6 Deletes, versions and lifecycle cost · rules
- Delete writes a delete marker (with versioning) or removes the metadata row; the bytes are freed later by garbage collection, which checks that no version still points at them.
- Versioning protects against accidental overwrite and deletion; each version is a separate metadata row.
- Lifecycle rules move objects to colder tiers (fewer, denser, slower disks, or tape-like archives with retrieval delays) or delete them after a time.
9.7 The whole design after the deep dives
10. Interviewer follow-ups
One prefix gets millions of requests a second.
Metadata ranges split automatically under load; a CDN or cache in front of hot public objects absorbs reads. Clients spreading keys across prefixes helps on systems that partition by prefix.
A whole zone goes down.
Fragments are placed so that losing one zone leaves at least k of k+m for every chunk (for example 9 fragments over 3 zones, 3 each, with k = 6). Reads decode from the remaining zones; repair starts only if the zone stays down.
Replicate a bucket to another region.
Asynchronous replication driven by a change log of metadata commits; the destination applies them in order per key. Report replication lag; a region failover may lose the last few seconds of writes.
A customer accidentally deletes everything.
Versioning plus delete markers make it reversible; object lock (write once, read many) prevents deletion entirely for a retention period, for compliance.
How do browsers upload directly without your servers?
Your app server issues a presigned URL for one key and one operation, valid for minutes; the browser uploads straight to storage.
Why not store objects in the metadata database?
Different access patterns and economics: metadata needs fast, consistent, small updates; bytes need cheap, dense, sequential storage. Mixing them makes both worse.
Ten times the data.
Add storage nodes; the placement service spreads new chunks onto them, and a background rebalancer moves some old ones. Metadata ranges split. Cost pressure pushes more data to erasure coding and colder tiers.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| Disk dies | Nothing | Node health, read errors | Erasure coding; distributed repair |
| Silent corruption | Nothing | Checksums on read and scrub | Rebuild the fragment from others |
| Zone outage | Slower reads | Zone health | Placement across zones; decode from survivors |
| Metadata shard leader lost | Seconds of retries for its keys | Consensus heartbeats | Replicated ranges elect a new leader |
| Front end crash mid-upload | A failed request; client retries | Error rates | Metadata committed last; orphan GC |
| Bug deletes data | Missing objects | Audit, delete-rate anomalies | Versioning, delayed GC, object lock |
12. Wrap-up and the clock
Trade-offs we made:
- Metadata and bytes stored separately, each in the structure that suits it.
- Erasure coding for cost, with copies for small or hot data.
- Range-partitioned metadata for ordered listings, at the cost of split machinery.
- Bytes first, metadata commit last: no partial objects, some garbage to collect.
- Lazy deletion through garbage collection.
What we'd watch: chunks below full redundancy (by how many fragments missing) · repair backlog and rate · scrub coverage · metadata latency p99 per range · request errors by type · storage overhead ratio.
At 10×: more nodes and metadata ranges; durability depends on keeping repair faster than failures as disks get bigger.
A 60-minute interview
| 0–5 | Frame: size, traffic, consistency, durability |
| 5–10 | Must do, how well, numbers |
| 10–15 | Nouns, API, put data flow |
| 15–25 | First version: metadata vs bytes, put, get, list |
| 25–50 | Deep dives: erasure coding (9.1), repair (9.2), metadata (9.4) |
| 50–60 | Multipart, follow-ups, wrap-up |
A 90-minute interview
| 0–8 | Frame, including versioning and access |
| 8–15 | Must do, how well, numbers |
| 15–22 | Nouns, API, data flow |
| 22–35 | First working version |
| 35–75 | Deep dives 9.1–9.6 and the final picture |
| 75–85 | Follow-ups: zone loss, hot prefixes, regions |
| 85–90 | Trade-offs, metrics, 10× |
| Level | What a strong answer shows |
|---|---|
| Mid-level | Metadata separate from bytes, replication across zones, a put/get/list API, multipart upload. |
| Senior | Erasure coding with placement across failure domains, commit-last writes, range-partitioned metadata for listings, scrubbing and repair. |
| Staff+ | Durability as failure rate versus repair speed, cost per byte as a design driver, small-object packing, read-after-write consistency at scale, lifecycle economics. |
Common mistakes:
- Storing bytes in the metadata database.
- Three copies with no discussion of cost.
- Hash-partitioned metadata, then promising fast prefix listings.
- Writing metadata before the bytes are durable.
- Treating disk failure as a rare incident.
13. Try it, check yourself, read more
Erasure coding lab. Pick a scheme, then click storage nodes to fail them. Can the chunk still be read?
Teal = data fragment, amber = parity fragment (or a full copy for replication). Any k of the k + m fragments rebuild the chunk.
What does 6+3 erasure coding give you?
When is an object's metadata committed?
How should metadata be partitioned so prefix listings are fast?
Why scrub data that nobody is reading?
| Resource | Why |
|---|---|
| Building and operating a pretty big storage system called S3 (Werner Vogels) | How S3's scale, hard drives and durability work in practice. |
| S3 strong consistency (AWS) | Read-after-write consistency for all operations. |
| Erasure Coding in Windows Azure Storage (USENIX ATC 2012) | Local reconstruction codes and their cost trade-offs. |
| Backblaze on Reed-Solomon | A readable explanation of erasure codes in a real storage service. |
| Ceph architecture | An open-source object store: placement groups, CRUSH and recovery. |
| Multipart upload (AWS) | The upload protocol for large objects. |