Design object storage (Amazon S3): the system design interview walkthrough
The request to design object storage like Amazon S3 sounds like one line — put a file, get it back by name — and that one line hides the whole interview. Write a file to one disk and you reach for open() and write(); store exabytes of immutable blobs at eleven nines of durability, under a flat bucket-and-key namespace, with huge read and write throughput and objects ranging from a few bytes to several terabytes, and it becomes a durability-plus-metadata-at-scale problem with a dozen sharp edges. This is one of the most common object storage system design prompts because it forces every distributed-storage muscle at once: durable writes, a namespace that scales, large-object handling, and redundancy across failure domains.
The round turns on one structural insight. A single filesystem keeps the bytes and the directory entry together on one machine; an object storage system design instead separates the metadata service — the map from key to where the bytes live — from the data plane, the blob bytes themselves spread across many storage nodes. Get that one reframe — the key-to-location map is one system and the bytes are another, each scaling independently — and the rest of the system design interview is deciding how to shard that metadata, how to chunk and encode the bytes so a disk or a rack can die without losing an object, and how a reader never sees a half-written blob storage object. This walkthrough follows the path a strong candidate draws on the whiteboard — requirements, estimate, API and data model, high-level design, then the deep dives — and at each step it names the follow-up the interviewer uses to tell a memorised answer from an engineer who understands the tradeoff.
How the object storage round is actually run
An object storage round runs about 60 minutes on a shared canvas — you draw the boxes and arrows while you talk, and the interviewer interrupts. It is a favourite because the one-line problem opens onto deep water fast: the moment you draw "client puts a file, client gets a file," the question becomes where do the bytes actually live, and how do you find them again, and every answer after that (durability, the namespace, large objects) compounds from that split between metadata and data.
The interviewer rarely lets "store the bytes, remember the key" stand. "A disk dies — then a whole rack in one availability zone goes dark. Is any object lost, and exactly how much extra storage did you pay to make the answer no?" That question — durability under correlated failure, and its cost — is the one a copied answer can never rehearse, and the one this round is really testing.
The rounds here are modelled on how the loop runs, not scraped from any candidate's recall. What matters for prep is the shape: a blank canvas, a clock, and a follow-up after every decision you make.
The five-step framework for any HLD round
Every high-level design answer fits the same spine. Say it out loud at the start so the interviewer knows you have a plan, then walk it:
- Clarify requirements — functional and non-functional, and what you are explicitly not building.
- Estimate — total data, object count, size range, durability target and throughput, to justify every later choice.
- High-level design — the boxes and arrows: clients, API tier, metadata service, data nodes, a placement coordinator.
- Deep dive — the two or three decisions that actually decide this system.
- Follow-ups — bottlenecks, failure modes, and what you would change at ten times the scale.
What the debrief measures
The score is not "right or wrong design." A good HLD debrief reads on four dimensions, and every move below maps to one: problem solving (did you separate the metadata service from the data plane and reach erasure coding for durability), communication (did you say why a flat key-to-object map beats a real directory tree in a sentence), technical execution (sharded metadata, chunking, erasure coding across availability zones, multipart upload, immutability and versioning), and time management (did you spend your 60 minutes on the namespace, durability, and the large-object write path, not on bucket-policy syntax). Reading a walkthrough teaches the design; it does not teach you to deliver it on the clock while someone pushes back.
Step 1 — Requirements: what object storage must do
Spend the first few minutes here. Jumping to boxes before you have scoped the problem is the fastest way to design the wrong system.
| Type | Requirement |
|---|---|
| Functional | Clients create buckets and PUT / GET / DELETE objects addressed by a (bucket, key) pair; LIST objects in a bucket by key prefix; objects range from a few bytes to several terabytes; support huge read and write throughput across many buckets and clients. |
| Functional (optional) | Object versioning, lifecycle and tiering to colder storage, server-side encryption, access policies — call these out, then scope most of them for later. |
| Non-functional | Extreme durability (eleven nines — effectively never lose an object); high availability; strong read-after-write consistency for new objects; near-infinite horizontal scale in both bytes and object count; high aggregate throughput. |
The sentence that earns points: "The hard part isn't the put/get API, it's eleven nines of durability over exabytes while a flat namespace of trillions of keys stays fast — so the core of the design is a sharded metadata service separated from a data plane that chunks and erasure-codes the bytes, not one filesystem on one box." Say that and you have framed the entire design before drawing a box.
Step 2 — Capacity estimation (back of the envelope)
Estimation here is about proving two things: the hard axes are durability at scale and metadata at scale, not clever put/get logic.
| Quantity | Assumption | Result |
|---|---|---|
| Total data | exabytes of stored objects | chunk every object and erasure-code the fragments across disks, racks and availability zones |
| Object count | trillions of keys | the metadata map is itself a huge store — it must be sharded and horizontally partitioned, not a single database |
| Object size | a few bytes up to several terabytes | small objects must not waste a whole chunk; large objects need multipart upload and streaming |
| Durability target | eleven nines (~99.999999999%) | redundancy across independent failure domains plus background scrubbing and repair |
| Throughput | millions of requests/sec, GB/sec of transfer | the metadata lookup must be small and fast; byte streaming must bypass it and go straight to data nodes |
Two conclusions fall out, and you should say both out loud. First, durability is the spine — no single disk, rack or availability zone can be a single point of loss, so objects are split into chunks and spread with redundancy across failure domains, and a background process continuously scrubs and rebuilds lost fragments. Second, the metadata is itself a scaling problem — a trillion-key map cannot live on one machine, so it is sharded by bucket-and-key hash, and the request path keeps the small metadata lookup separate from the large byte transfer so each scales on its own axis. You have now pointed the interview at the real deep dives — the metadata service and durability — before anyone asked.
Step 3 — API and data model
The API is deliberately small: clients put and get whole objects by key, list by prefix, and upload very large objects in parts.
| Operation | Shape |
|---|---|
PUT(bucket, key, bytes) |
writes a new object (or a new version); returns an ETag/version once every durable fragment is written. |
GET(bucket, key) |
resolves the key through metadata to chunk locations, then streams the bytes from the data nodes. |
DELETE(bucket, key) |
removes the key (or adds a delete marker when versioning is on); space is reclaimed asynchronously. |
LIST(bucket, prefix) |
returns keys sharing a prefix — the only "directory" the system has — in sorted order, paginated. |
initiate / upload_part / complete
|
multipart upload: open an upload id, push parts in parallel, then a complete call stitches them via the manifest. |
The data model is thin, and the key fact is that the key names the object but says nothing about where the bytes live — that mapping is what the metadata service holds:
| Entity | Notes |
|---|---|
bucket |
a flat container for keys; owns region, policy and versioning settings — not a directory tree. |
object |
(bucket, key), version_id, size, checksum, an ordered chunk list, and user metadata; immutable once written. |
chunk |
a fragment of an object's bytes: chunk_id, its storage location(s), and the erasure-coding group it belongs to. |
Step 4 — High-level design: metadata service plus data plane
Now the boxes. A client calls an API tier behind a load balancer; from there the request splits in two. Around that split sit the pieces that make durability and scale work:
- Clients and API / load balancer: terminate the request, authenticate it, and route it — a metadata lookup for the key, then a byte transfer to or from the data nodes.
-
Metadata service: a sharded, horizontally partitioned key-value store mapping
(bucket, key, version)to the object's size, checksum and chunk locations; sharded by a hash of bucket-and-key so no single node is the bottleneck, and the home ofLIST-by-prefix. - Data nodes: the data plane — commodity servers with disks that store the actual chunks, erasure-coded and spread across racks and availability zones; byte streaming goes straight here, not through the metadata tier.
- Placement / coordinator: decides which failure domains a new object's chunks land in, tracks node health, and drives background scrubbing and repair that rebuilds fragments lost to dead disks.
Say explicitly that the request path splits in two — a small, fast, sharded metadata lookup (key → where the bytes live) and then a large byte stream from the data nodes — and that keeping them separate is exactly what lets each scale independently: metadata grows with the number of objects, data grows with the number of bytes. Naming that unprompted is a signal the interviewer is listening for.
Deep dive: the metadata service and the flat namespace
This is the first decision that actually decides the round. How do you name trillions of objects without a directory tree, and why does that shape everything else?
A bucket is not a folder hierarchy — it is a flat map from key to object. There are no real directories; a key like photos/2026/trip.jpg is a single opaque string, and the slashes are just characters. The metadata service holds that map, and three properties fall straight out of the choice:
| Property | Why the flat namespace plus sharded metadata gives it |
|---|---|
| Scale | A trillion-key map cannot sit on one machine, so it is sharded by a hash of bucket-and-key across many nodes — add shards to add capacity, and no single node owns the whole namespace. |
| Prefix listing |
LIST(bucket, "photos/") is a range scan over sorted keys, not a walk of a directory tree — "folders" are an illusion the client draws from shared prefixes. |
| Separation from bytes | Metadata stores only the small facts — key, size, version, checksum, chunk locations — so the lookup stays tiny and fast while the bytes stream from data nodes on a different path. |
Sharding by key hash is also how the namespace stays fast. Because the shard is chosen by hashing the full key, load spreads evenly even when many keys share a prefix — but it also means a prefix scan may have to fan out across shards, which is the cost you call out. The metadata layer is the throughput bottleneck of the whole system, which is why it is partitioned, kept small, and never made to carry the object bytes.
"There are no real folders — so how does
LIST bucket/photos/actually work on a flat namespace?" It is a range scan: keys are stored sorted, and a prefix query returns the contiguous run of keys that start withphotos/, paginated. The directory you see in a console is reconstructed from shared prefixes on the client side — the store itself has one flat, sharded key-to-object map and no tree to walk.
System Design
HLD · Storage & files · 60 min
Design object storage like Amazon S3 out loud, with follow-ups
Deep dive: durability with erasure coding
The metadata tells you where the bytes are; this decides whether the bytes survive at all. The follow-up is always the correlated-failure case — a disk, then a whole rack.
Every object is split into chunks, and those chunks are spread across many disks, racks and availability zones so that no single failure domain holds enough to lose the object. The question is how you add the redundancy, and there are two answers:
| Approach | How | Cost / what it tolerates |
|---|---|---|
| Replication (3×) | Keep three full copies of every chunk on three independent nodes. | Simple and fast to read, but 200% storage overhead; tolerates losing any two of the three copies. |
| Erasure coding (e.g. 10 + 4) | Split a chunk into 10 data fragments and compute 4 parity fragments; store all 14 across different failure domains, reconstruct the chunk from any 10 of the 14. | Only ~40% overhead, and survives the loss of any 4 fragments at once — a disk and a rack can both fail and the object is still whole. |
Erasure coding is how you reach eleven nines without paying for triple copies: the same safety that 3× replication buys at 200% overhead, a 10-of-14 scheme buys at ~40% while tolerating more simultaneous losses. The price is CPU and read amplification — reconstructing a fragment means reading 10 others — so hot small objects are sometimes replicated instead, and cold bulk data is erasure-coded. A background scrubber continuously verifies checksums and, whenever a disk or node dies, repairs the missing fragments from the survivors before a second failure can catch up with the first, which is what keeps the durability maths honest over time.
"A disk fails — then a whole rack in one availability zone goes dark. Is any object lost, and how much extra storage did that safety cost?" With a 10-of-14 erasure-coding scheme spread across failure domains, losing a disk and a rack can take out several fragments and the object still reconstructs from any 10 of the 14 — nothing is lost — and the scrubber rebuilds the missing fragments afterwards. The cost is ~40% overhead, versus the 200% you'd pay for 3× replication to tolerate fewer simultaneous losses.
System Design
HLD · Storage & files · 60 min
Defend erasure coding versus replication out loud
Deep dive: large objects, the write path and consistency
Durability decides whether the bytes survive; this decides how a five-terabyte object gets in, and what a reader sees when two clients write the same key at once. The follow-up is always the big-object write and the race.
Two ideas carry this deep dive. First, multipart upload: a huge object is split into parts that clients upload in parallel to different data nodes, each part durably written on its own; a final complete call stitches the parts together via a manifest that records the ordered chunk list. That is how you move a terabyte-scale object without one giant, fragile stream, and a failed part is simply re-uploaded. Second, immutability: an object is never edited in place.
| Mechanism | What happens | Why it matters |
|---|---|---|
| Multipart upload |
initiate returns an upload id; parts upload in parallel; complete writes a manifest stitching them in order. |
Large objects move in parallel and survive partial failure — re-upload one part, not the whole object. |
| Immutability | A "overwrite" writes a brand-new object (new chunks) and only then flips the metadata pointer to it. | A reader always sees a complete old object or a complete new one — never a half-written blob. |
| Versioning + pointer flip | The metadata key keeps an ordered list of version_ids; the pointer flip is the atomic commit. |
Enables versioning and safe concurrent writes; the last successful complete wins the pointer. |
Because writes are immutable and the pointer flip is the atomic moment of commit, the consistency model is strong read-after-write for new objects: a GET issued after a successful PUT returns the new bytes, because the PUT only returns success once the pointer references fully-written, durably-stored chunks. If two clients PUT the same key concurrently, both write their own new objects and the last pointer flip wins — every reader sees one whole consistent version, never a blend of the two.
"How do you upload a five-terabyte object, and what does a reader see if two clients
PUTthe same key at once?" You use multipart upload — split it into parts, push them in parallel, then one complete call stitches them via a manifest. Objects are immutable, so each concurrentPUTwrites a brand-new object and the last successful pointer flip wins; a reader always sees one complete version, never a half-written blob, becauseGETresolves the pointer only after the bytes are fully durable.
The follow-ups that decide the object storage round
Scan the probes from each deep dive and they rhyme. Master these five and you are ready for a version of the question that is not drawn exactly like this one.
- "Why split the metadata service from the data plane?" → A key-to-location map grows with object count; the bytes grow with volume — separating them lets a small, sharded lookup and a large byte stream each scale on their own axis. (problem solving)
-
"There are no folders — how does prefix listing work?" → A bucket is a flat, sharded key-to-object map;
LISTis a range scan over sorted keys, and "directories" are just shared prefixes drawn client-side. (communication) - "A disk and a rack both fail — is an object lost?" → Not with erasure coding across failure domains: a 10-of-14 scheme reconstructs from any 10 fragments and the scrubber repairs the rest, at ~40% overhead versus 200% for 3× replication. (problem solving)
- "How do you store a five-terabyte object?" → Multipart upload: parts in parallel to different nodes, then a complete call stitches them via a manifest; re-upload a failed part, not the whole object. (technical execution)
-
"Two clients write the same key at once — what does a reader see?" → Objects are immutable, so each
PUTwrites new chunks and the last atomic pointer flip wins; strong read-after-write means a reader sees one complete version, never a half-written blob. (technical execution)
Each maps to a debrief dimension. Reading this list teaches you the answers; it does not teach you to deliver them while someone interrupts — that only comes from saying them out loud.
How to prepare: a one-week plan
You do not need fifty systems. You need this one cold and the framework automatic, so the next "design X" is just new nouns in the same five steps.
- Days 1–2: Learn the five-step framework until it is reflex. Do object storage end to end on paper — requirements, estimate, API, data model, the metadata-plus-data-plane boxes.
- Days 3–4: Drill the namespace deep dive: the flat key-to-object map, sharding by key hash, prefix listing as a range scan, and why metadata is separated from the bytes. Say each tradeoff in one sentence.
- Days 5–6: Own durability and the write path — replication versus erasure coding, scrubbing and repair, multipart upload, immutability and versioning, strong read-after-write. Answer each of the five follow-ups out loud.
- Day 7: A full timed 60-minute round, narrating the whole way, with someone (or something) interrupting. This is the rep that counts.
This post is the deep walkthrough behind one row of the broader Top 25 system design interview questions — once object storage is automatic, the same spine carries the rest of the list.
Frequently asked questions
How do I design object storage like Amazon S3 step by step?
Follow the five-step HLD framework. Clarify requirements (buckets, PUT/GET/DELETE/LIST by prefix, byte-to-terabyte objects, eleven nines of durability, strong read-after-write); estimate to show the hard axes are durability over exabytes and metadata over trillions of keys, not clever put/get logic; define the small API (PUT, GET, DELETE, LIST, multipart) with a thin bucket/object/chunk model; draw the design (clients to an API tier that splits each request into a sharded metadata lookup and a byte stream to data nodes, with a placement coordinator and background repair); then deep-dive the decisions that matter — the metadata service and flat namespace, durability with erasure coding, and the large-object write path with immutability. Finish on the follow-ups about correlated failure and concurrent writes.
Why separate the metadata service from the data plane?
Because the two scale on different axes. The metadata — the map from (bucket, key, version) to chunk locations — grows with the number of objects, so it is sharded by key hash across many nodes and kept small and fast. The bytes grow with total volume, so they live on data nodes and are streamed directly, never through the metadata tier. Keeping the small, fast key lookup separate from the large byte transfer lets each layer scale independently, and it is why a single metadata lookup can stay quick even when the object sits across dozens of data nodes.
How does listing work if object storage has no real folders?
A bucket is a flat map from key to object — there are no directories, and a key like photos/2026/trip.jpg is just one opaque string whose slashes are ordinary characters. Keys are stored sorted, so LIST(bucket, "photos/") is a range scan that returns the contiguous run of keys beginning with that prefix, paginated. The folder tree you see in a console is reconstructed on the client from shared prefixes; the store itself has one flat, sharded key-to-object namespace and no tree to walk, which is what lets it hold trillions of keys.
What is the difference between replication and erasure coding for durability?
Both spread an object's chunks across independent failure domains; they differ in cost. Three-times replication keeps three full copies, which is simple and fast to read but carries 200% storage overhead and tolerates losing any two copies. Erasure coding — say 10 data plus 4 parity fragments — reconstructs each chunk from any 10 of the 14 fragments, so it survives up to four simultaneous losses at only about 40% overhead. Erasure coding is how exabyte-scale systems reach eleven nines affordably; the trade is extra CPU and read amplification when a fragment has to be rebuilt.
How does multipart upload handle very large objects and consistency?
A multi-terabyte object is split into parts that clients upload in parallel to different data nodes; each part is written durably on its own, and a final complete call stitches them via a manifest recording the ordered chunk list, so a failed part is simply re-uploaded rather than restarting the whole object. Objects are immutable: an overwrite writes brand-new chunks and only then flips the metadata version pointer atomically, so a reader never sees a half-written blob. That gives strong read-after-write consistency — a GET after a successful PUT returns the new bytes — and when two clients write the same key, the last pointer flip wins and every reader sees one complete version.
How do I practise a system design interview out loud?
Reading a walkthrough is not the same as defending a design while someone interrupts. On Zynter, the HLD track runs live voice rounds modelled on how real system design loops run — you talk through a shared design on the canvas, and a voice interviewer pushes with the follow-ups above one at a time, then gives you a debrief on problem solving, communication, technical execution, and time management. The score is a practice signal, not a hiring decision.
Practise it on Zynter
You can name every chunk. The round is won on durability and the metadata split.
Pick the Object Storage round, draw the metadata-plus-data-plane design on the canvas, and defend it out loud to a voice interviewer that keeps asking "a disk and a rack both fail — is it lost?" and "two clients write the same key — what does a reader see?" — then read the debrief. Zynter.ai is on-demand mock interviews modelled on real company loops, with 150+ rounds across 20 companies including the HLD system design track (Sep 2026).
Practise the Object Storage round →
Browse all interview tracks




Top comments (0)