System design interview
Design an object storage service
Metadata and data placement, erasure coding, multipart upload, listing.
At a glance
- Advanced
- Storage, Sharding, Consistency
- API gateway · Cluster map · Storage nodes · Metadata index · Health monitor · Repair queue · Repair worker
Press Play to follow a request through the design, or step through it.
- Client and edge
- Service
- Data store
- Cache and queue
- The client sends a put for a bucket, a key and the bytes.
- The gateway hashes the new object's id to a placement group and asks which nine nodes serve it.
- The map returns nine nodes on nine different racks. The gateway also keeps a local copy of it.
- It cuts the object into 6 data chunks, computes 3 parity chunks, and writes all 9 to their nodes, each with a checksum.
- The nodes confirm that the chunks are on disk. The gateway waits for every confirmation.
- Now that the data is durable, it commits one metadata row for the key, pointing at the new object.
- The metadata shard confirms after a majority of its replicas have the row. From this moment the object is visible.
- The gateway returns success to the client.
- Client
- API gateway: encode, decode
- Cluster map: group to nodes
- Storage nodes: disks, chunks
- Metadata index: bucket and key
- Health monitor
- Repair queue: worst first
- Repair worker
On this page
In 2 minutes
- The numbers. Disks fail weekly, so repair is routine work and its speed is what protects durability.
- Protecting data. Erasure coding, 6 data plus 3 parity.
- Sharding the key index. Split by key range.
- Making an object visible. Write chunks first, then commit the row.
- Reading the index. Answer from the shard leader.
Read the 2-minute answerHide the 2-minute answer
I would split the system in two. A metadata index keeps one small row per object, sorted by bucket and key and sharded by key range, each shard replicated by consensus. At 20 billion objects that is about 4 TB, or 200 GB in each of 20 shards, and listing by prefix is a seek and a scan in one shard. The data lives on storage nodes as erasure-coded chunks, 6 data plus 3 parity, which costs 1.5 times the space and tolerates 3 lost chunks.
Objects hash into 100,000 placement groups. A small cluster map says which nine nodes on nine racks hold a group, so a disk failure changes the map and no metadata. A put writes the chunks first, then commits the metadata row, so a visible object always has durable data and the leader answers reads, which gives read-after-write. With 3,000 disks, about 60 fail each year, so a health monitor queues repairs, the worst groups first.
Rebuilding a disk takes about 5.6 hours, and shortening that window helps durability most. Checksums and scrubbing catch silent corruption. Large objects go up as parallel parts and become visible with one commit.
Step 1 of 6: Clarify the scope
Questions to ask first
Ask these before you draw anything. Guess why each answer changes the design, then open it to check.
How many objects, how big are they, and how fast does the total grow?
What durability and availability do we promise?
After a successful write, must a read and a listing see the new object immediately?
How large can one object be, and can it change in place?
Do we need versioning or copies in other regions?
Requirements
Functional
- Put, get and delete an object by bucket and key, and read just a byte range of it.
- List the keys in a bucket in order, by prefix, a page at a time.
- Upload objects up to 5 TB in parts, with parts that can be retried and uploaded in parallel.
- Overwrite a key so that readers see the old object or the new one in full, never a mixture.
Non-functional
- Durability: lose no more than 1 object in 100 billion per year.
- A new object is visible to a get and a list as soon as the put returns.
- Time to first byte for a 1 MB get: p99 under 150 ms.
- Store 40 PB and serve 100,000 requests a second at peak.
Step 2 of 6: Size it
Show the mathsHide the maths
These numbers are assumptions for the exercise, not facts about a real system:
- The service holds 20 billion objects with a mean size of 2 MB, which is 40 PB. A metadata row takes 200 bytes.
- Data is stored with a 6 data plus 3 parity code, so any 6 of 9 chunks rebuild an object. Disks hold 20 TB and a disk fails 2% of years.
- The cluster can rebuild lost data at 1 GB/s without hurting normal traffic. Metadata is split into 20 shards.
The arithmetic:
- Raw storage
- 40 PB × 9 ÷ 6 = 60 PB with the code; 40 PB × 3 = 120 PB with 3 copies
- 60 PB, half the space of three copies
- Disks and failures
- 60 PB ÷ 20 TB = 3,000 disks; × 2% a year
- about 60 failures a year, roughly one a week
- Metadata index
- 20B × 200 bytes = 4 TB; ÷ 20 shards; 100,000 requests/s ÷ 20
- 200 GB and about 5,000 lookups/s per shard
- Repair of one lost disk
- 20 TB to rebuild ÷ 1 GB/s = 20,000 s; reads are 6 × 20 TB = 120 TB
- about 5.6 hours, and 120 TB of reads
- Chance of losing an object, if failures were independent
- p = 2% × 5.6 h ÷ 8,760 h ≈ 1.3 in 100,000 per disk per window. A 9-chunk group dies if 4 fail: 126 × p^4, over 1,564 windows a year
- ≈ 5 in 10^15 a year, against ≈ 3 in 10^12 for 3 copies. Real risk is correlated failures
Step 3 of 6: Sketch the path of one request
Step through the diagram at the top of the page to watch a request move through these parts.
The key idea is to keep metadata and data apart. The Metadata index holds one small row per object: bucket and key, size, version, and the placement group where its chunks live. It is kept in key order and split into shards, so a key is found and a list can be served. The data itself lives as chunks on the Storage nodes, which are built for streaming big blobs.
A Client talks to the API gateway. To write, the gateway hashes the object to a placement group and asks the Cluster map which nine nodes in that group to use. It cuts the object into 6 data chunks, computes 3 parity chunks, and writes the 9 chunks to 9 nodes on different racks. Only after the chunks are safe does it commit the row to the metadata index, so an object becomes visible only when its data is durable.
To read, the gateway looks up the row, then fetches the data chunks in parallel and joins them. The Health monitor watches heartbeats from the nodes. When a disk is gone, it updates the map and queues repair tasks on the Repair queue. A Repair worker rebuilds each missing chunk from the surviving ones.
Other paths through the system
Read an object
- The client asks for a key.
- The gateway reads the row from the metadata shard that owns the key, to get the object id, size and group.
- The row comes back from the shard's leader, so it is never older than the last committed write.
- Using its cached map, the gateway asks the group's nodes for the 6 data chunks in parallel, plus one spare.
- The first six answers arrive, each checked against its checksum, and the gateway joins them.
- The bytes stream back to the client while later chunks are still arriving.
Repair a lost disk
- Every node sends a heartbeat. One disk has been silent for 10 minutes, long enough to rule out a reboot.
- The monitor marks the disk down and picks a replacement node, on a free rack, for each of the groups that used it.
- It queues one repair task per affected group, with the groups that lost the most chunks first.
- A repair worker takes the next task, within its rate limit.
- It reads six surviving chunks of the group, rebuilds the missing one by decoding, and writes it to the replacement node.
- The new node confirms the chunk, and its next heartbeat reports it. The group is whole again.
Step 4 of 6: Go deep on the hard part
Separating metadata from data placement
Why split the metadata from the data, and how do you decide where a billion objects live?The two have different shapes. Metadata is tiny rows that need ordered lookups, prefix scans and strong consistency. Data is huge, unchanging blobs that need disk throughput.
The two have different shapes. Metadata is tiny rows that need ordered lookups, prefix scans and strong consistency. Data is huge, unchanging blobs that need disk throughput. Keeping them apart lets us put metadata on fast flash in a replicated, sorted store, and data on cheap, large disks.
The metadata index is sorted by bucket and key, and it is split by key range, not by hash. A hash spreads load evenly, but a listing by prefix would then have to ask every shard and merge. With ranges, a prefix usually sits in one shard. The price is hot ranges, such as keys that begin with a timestamp, so shards split when they grow past a size or load limit. At 20 billion rows of 200 bytes, that is 4 TB in 20 shards.
Data placement uses indirection. Recording nine nodes on every row would be big and would need rewriting whenever a disk fails. Instead, hash the object id into one of 100,000 placement groups. A small cluster map says which nine nodes hold each group. The map is a few megabytes, every gateway keeps a copy, and it changes only when disks fail or are added. A failure changes the map for the groups on that disk, and no metadata row changes.
The numbers fit: 60 PB ÷ 100,000 groups is 600 GB a group, or about 67 GB per node. A disk then belongs to 100,000 × 9 ÷ 3,000 = 300 groups, and 300 × 67 GB gives its 20 TB. Those 300 groups have their partners spread across the cluster, so a repair is read from many disks at once.
Write order. Write the chunks first and commit the metadata last. A crash in between leaves orphan chunks that no row points to. A garbage collector removes chunks without a row once they are a day old. Readers can never see a half-written object, because they always go through the row.
Very small objects do badly with a 6-way split, since each chunk would be a few hundred bytes. Pack small objects into larger blobs, or keep three copies of them.
Replication, erasure coding and repair
How do you keep data safe through disk failures, and what does it cost compared with keeping copies?Three copies. Simple.
Three copies. Simple. A read goes to any one node, and repair is a plain copy. It tolerates the loss of 2 copies, and it costs 3 times the space: 120 PB for our 40 PB.
Erasure coding, 6 data plus 3 parity. Cut the object into 6 data chunks, and compute 3 parity chunks with an error-correcting code. Any 6 of the 9 chunks rebuild the whole object, so any 3 losses are survivable. It costs 9 ÷ 6 = 1.5 times the space, 60 PB. The prices are real. A read needs 6 nodes and not 1, so ask for 7 and use the first 6 to arrive, which protects tail latency. A write needs the encoding work and 9 writes. Repair needs reading 6 chunks to rebuild 1, so rebuilding a 20 TB disk reads 120 TB.
Durability arithmetic. At 1 GB/s, a rebuild takes about 5.6 hours. In that window, a disk fails with probability p ≈ 1.3 in 100,000. A 9-chunk group is lost only if 4 chunks fail inside the same window, which is about 126 × p^4. Over the 1,564 windows in a year, that is about 5 in 10^15 per object. Three copies need all 3 to fail, about p^3 per window, or 3 in 10^12 a year. So the code is both smaller and safer, in this model.
The model assumes failures are independent, and they are not. A rack loses power, a batch of disks has the same firmware bug, or an operator makes a mistake. So place the 9 chunks on 9 different racks, ideally across 3 zones, and test the repair path often.
The window is the lever. Loss needs 4 failures inside one window, and each window is shorter and more frequent when repair is faster, so halving the repair time cuts the yearly risk about 8 times. Repair the groups with the most missing chunks first. Wait 10 minutes before declaring a disk dead, so a reboot does not trigger a 120 TB read.
Disks also rot silently. Store a checksum with every chunk, check it on each read, and run a slow background scrub that reads every chunk. A bad chunk is treated as lost and rebuilt.
Multipart upload, listing and read-after-write
How do you upload a 5 TB object, list keys by prefix, and make sure a new object is visible at once?Multipart upload. The client asks to start an upload and gets an upload id.
Multipart upload. The client asks to start an upload and gets an upload id. It then sends parts, each with a number, in parallel and with independent retries. Each part is stored as chunks like any object, with a checksum. When the client calls complete with the list of parts, the service commits one metadata row that lists the parts in order, and that single write makes the object appear. An abort, or an upload left for a week, has its parts deleted. If a limit is 10,000 parts, a 5 TB object needs parts of at least 500 MB. A range read finds which parts and chunks hold the requested bytes.
Listing. A list call has a bucket, a prefix, a delimiter such as a slash, a start point and a page size. Because the index is sorted, the shard seeks to the prefix and scans forward. With a delimiter, when it reaches a key like photos/2024/a.jpg under the prefix photos/, it returns the common prefix photos/2024/ once and then seeks past every key that starts with it. This is how a bucket looks like folders without ever storing folders. The next page is a token holding the last key returned, not an offset, so pages stay correct while keys are added.
Read-after-write. Each metadata shard is a small group of replicas using a consensus protocol. A commit returns after a majority has written the row. Reads and lists for that shard are answered by the leader, or by a follower that has first checked that it has applied everything the leader has committed. So any request made after a put returns will see the row. Because the chunks were written first, a visible row always points to readable data.
Overwrites. An overwrite writes a new object with a new id and then switches the row to point at it in one atomic write. A reader gets the whole old object or the whole new one, since both are immutable. The old chunks are removed later by garbage collection.
A put that must not replace an existing key is a conditional insert on the same row. If we allowed stale reads from any follower, reads would scale further but could miss a new object, so say that choice out loud.
Step 5 of 6: Name the trade-offs
- Erasure coding, 6 data plus 3 parity
- Three full copies
It needs half the space and, in this model, loses data less often. The cost is encoding work, 6 reads per get and 6 times more reading to repair.
- Split by key range
- Split by hash of the key
A prefix listing touches one shard and pages in order. The cost is hot ranges that must be split as they grow.
- Write chunks first, then commit the row
- Write the row first
A visible object always has durable data. The cost is orphan chunks after a crash, which garbage collection must clear.
- Answer from the shard leader
- Answer from any follower
A new object is always seen. The cost is that the leader carries the read load, and a failover pauses reads briefly.
- Wait 10 minutes of silence
- Repair at the first missed heartbeat
A reboot does not cause 120 TB of reads. The cost is 10 more minutes with a chunk missing.
Step 6 of 6: Say what breaks next
Follow-up questions an interviewer asks once the design is on the board. Answer each one out loud, then open it to compare.