System design interview

Design an object storage service

Metadata and data placement, erasure coding, multipart upload, listing.

At a glance

Level
Advanced
Topics
Storage, Sharding, Consistency
Main parts
API gateway · Cluster map · Storage nodes · Metadata index · Health monitor · Repair queue · Repair worker
What you'll learn
Practise this problem
Overview
ClientAPI gatewayCluster mapStoragenodesMetadataindexHealthmonitorRepair queueworst firstRepairworkerClientAPI gatewayencode, decodeCluster mapgroup to nodesStorage nodesdisks, chunksMetadata indexbucket and keyHealth monitorRepair queueworst firstRepair worker

Press Play to follow a request through the design, or step through it.

  • Client and edge
  • Service
  • Data store
  • Cache and queue
  1. The client sends a put for a bucket, a key and the bytes.
  2. The gateway hashes the new object's id to a placement group and asks which nine nodes serve it.
  3. The map returns nine nodes on nine different racks. The gateway also keeps a local copy of it.
  4. It cuts the object into 6 data chunks, computes 3 parity chunks, and writes all 9 to their nodes, each with a checksum.
  5. The nodes confirm that the chunks are on disk. The gateway waits for every confirmation.
  6. Now that the data is durable, it commits one metadata row for the key, pointing at the new object.
  7. The metadata shard confirms after a majority of its replicas have the row. From this moment the object is visible.
  8. 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

Key ideas
  • 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 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?
Why it matters: Object count sets the size of the metadata index. Total bytes sets the number of disks. Many tiny objects change the design again.
What durability and availability do we promise?
Why it matters: The durability target decides the redundancy scheme and how fast we must repair after a disk dies.
After a successful write, must a read and a listing see the new object immediately?
Why it matters: Read-after-write consistency forces the metadata to commit in one place that reads also consult.
How large can one object be, and can it change in place?
Why it matters: Huge objects need a multipart upload. If objects are never edited, every version can be immutable, which simplifies everything.
Do we need versioning or copies in other regions?
Why it matters: Both are real extra work. Naming them keeps this design to one region with simple overwrites.

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 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

  1. The client asks for a key.
  2. The gateway reads the row from the metadata shard that owns the key, to get the object id, size and group.
  3. The row comes back from the shard's leader, so it is never older than the last committed write.
  4. Using its cached map, the gateway asks the group's nodes for the 6 data chunks in parallel, plus one spare.
  5. The first six answers arrive, each checked against its checksum, and the gateway joins them.
  6. The bytes stream back to the client while later chunks are still arriving.

Repair a lost disk

  1. Every node sends a heartbeat. One disk has been silent for 10 minutes, long enough to rule out a reboot.
  2. The monitor marks the disk down and picks a replacement node, on a free rack, for each of the groups that used it.
  3. It queues one repair task per affected group, with the groups that lost the most chunks first.
  4. A repair worker takes the next task, within its rate limit.
  5. It reads six surviving chunks of the group, rebuilds the missing one by decoding, and writes it to the replacement node.
  6. 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

InterviewerWhy 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

InterviewerHow 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

InterviewerHow 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

Protecting data
Chose
Erasure coding, 6 data plus 3 parity
Over
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.

Sharding the key index
Chose
Split by key range
Over
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.

Making an object visible
Chose
Write chunks first, then commit the row
Over
Write the row first

A visible object always has durable data. The cost is orphan chunks after a crash, which garbage collection must clear.

Reading the index
Chose
Answer from the shard leader
Over
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.

Declaring a disk dead
Chose
Wait 10 minutes of silence
Over
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.

How do you add storage nodes without moving everything?
Update the cluster map so that some placement groups move to the new nodes, a few at a time. Only those groups copy data, and the rest stay put. Use the same throttled workers as repair, so normal traffic is not hurt.
How would you support versioning?
Store every version as its own metadata row, with the key and a version id that sorts newest first. A get without a version reads the first row for the key. A delete adds a marker row, and a cleanup job removes old versions by a lifecycle rule.
How would you copy buckets to another region?
Ship the metadata commit log to the other region, and copy the matching objects in the background. The second region is eventually consistent, so it can lag by seconds or minutes. If you need a guaranteed copy before success, the put gets slower because it waits for a distant region.
How do you reclaim space after deletes?
A delete removes the row, and the chunks are now unreferenced. A garbage collector scans each group, compares chunk ids with the metadata, and deletes chunks with no row that are older than a safety delay. The delay protects against a put that has written chunks but not yet committed.

Free ATS check

See your resume the way an ATS reads it, then fix what it misses.

Drop in a PDF for a score out of 100, every fix that cost a point, and the line it belongs to. It runs in your browser, with no signup for the score.