Design object storage (like S3)
The interviewer asks
“Design an object storage service like Amazon S3. Users create buckets and PUT, GET, LIST and DELETE objects from a few bytes to many terabytes, with very high durability.”
What a strong answer looks like
A strong answer splits the system into three parts: a stateless front end (auth, routing), a metadata service (bucket and object index, sharded and strongly consistent), and a data service (storage nodes that hold immutable chunks). Objects are split into chunks, protected with erasure coding (for example 8+4 or 10+4 Reed-Solomon) spread across failure domains, and the metadata entry is written only after enough chunks are durable, which gives read-after-write consistency. Large uploads use multipart upload. The deep dives are durability maths (11 nines), erasure coding vs replication, the metadata design (sharded, ordered key index that supports prefix LIST), background repair and scrubbing, and garbage collection after deletes. Numbers: exabytes of data, trillions of objects, millions of requests per second.
Concepts to mention
1 Requirements
Functional
- Create and delete buckets; bucket names are globally unique
- PUT, GET (with byte ranges), HEAD and DELETE objects up to 5 TB
- Multipart upload for large objects with parallel part uploads and resume
- LIST objects in a bucket by prefix with pagination
- Optional versioning: keep old versions on overwrite and delete
- Access control with bucket policies and pre-signed URLs
Non-functional
- Durability: 99.999999999% (11 nines) per object per year
- Availability: 99.99% for reads and writes in a region
- Consistency: strong read-after-write for PUT and DELETE, and LIST reflects completed writes
- Scale: 100+ trillion objects, exabytes of data, millions of requests/s per region
- Latency: first byte in tens of ms for small objects; throughput scales with parallel connections
- Cost: storage overhead around 1.3-1.5x raw data, not 3x
Out of scope (say it out loud)
- Cross-region replication details (mention briefly)
- Storage class lifecycle to tape / archive
- Server-side query features (S3 Select, analytics)
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
PUT/{bucket} | Create a bucket |
PUT/{bucket}/{key} | Upload an object (single request up to ~5 GB); returns ETag |
GET/{bucket}/{key} | Download an object; supports Range header and versionId |
DELETE/{bucket}/{key} | Delete an object (or add a delete marker if versioning is on) |
GET/{bucket}?list-type=2&prefix=&continuation-token= | List keys by prefix, 1,000 per page, in lexicographic order |
POST/{bucket}/{key}?uploads | Start multipart upload; then PUT ?partNumber=N&uploadId= for each part and POST ?uploadId= to complete |
4 Data model
bucketsStrongly consistent replicated DB (Spanner-like / Raft groups)bucket_name (PK)owner_accountregionversioningpolicycreated_atSmall table (millions of rows), read on every request, so heavily cached by front ends.
object_indexSharded, range-partitioned KV (Raft-replicated, e.g. on RocksDB)(bucket, key, version_id) PKsizeetagcontent_typecreated_atchunk_layout (list of chunk/placement IDs)stateRange partitioning by (bucket, key) keeps keys sorted so prefix LIST is a range scan. Shards split when hot or large. Each shard is a Raft group of 3-5 replicas.
chunks / stripesStorage nodes (local disks, append-only files)stripe_idfragment indexdatachecksum (CRC32C)Each stripe = 10 data + 4 parity fragments on 14 different disks across racks and zones. Fragments live in large append-only volume files to avoid filesystem overhead for small objects.
placement_mapPlacement service (replicated, cached)volume_id → storage nodesdiskzonehealthMaps logical volumes to physical disks so data can move without rewriting object metadata.
multipart_uploadsobject_index shard (same as object)upload_id (PK)bucketkeyparts: [part_noetagsizechunk_layout]started_atParts are stored as normal chunks; complete = one metadata write that stitches the part list. Unfinished uploads expire by lifecycle rule.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
PUT a 50 MB object
- 1Client sends PUT /photos/2026/a.jpg with SigV4 signature; front end verifies the signature and checks bucket policy
- 2Front end asks the placement service for a stripe placement: 14 disks in different racks across 3 availability zones
- 3Front end streams the body, cuts it into stripes (e.g. 10 × 1 MB data blocks per stripe), computes 4 parity blocks, and writes each fragment with a checksum to its storage node in parallel
- 4When enough fragments are durable (all 14, or at least 12 with the rest queued for repair) the front end calls the metadata service
- 5Metadata service writes (bucket, key, version) → size, ETag, chunk layout into the Raft-replicated shard; this commit is the moment the object becomes visible
- 6Front end returns 200 with ETag (MD5 for single-part uploads); an 'ObjectCreated' event is published for notifications
- 7If the client crashes before commit, fragments exist without metadata; garbage collection removes such orphans later
GET with a byte range
- 1Front end authorises the request and looks up the latest version in the object index (cached briefly only if consistency allows)
- 2From the chunk layout it computes which stripes and fragments hold the requested bytes
- 3It reads the 10 data fragments directly (no decode needed); if a node is slow or down it reads parity fragments instead and decodes
- 4Checksums are verified on read; a mismatch triggers a repair ticket and the data is rebuilt from other fragments
- 5Bytes stream back to the client; large objects are served by many parallel range GETs from the SDK
7 Deep dives
Durability: erasure coding and failure domains
Three full copies give good durability but cost 3x storage. Erasure coding with Reed-Solomon 10+4 splits data into 10 fragments and adds 4 parity fragments; any 10 of the 14 rebuild the data. Overhead is 1.4x and it survives any 4 lost fragments, which is better than 3-way replication (survives 2). Fragments must be placed in independent failure domains: different disks, servers, racks and, for zone-level durability, 3 availability zones (for example 5+5+4 across zones, so losing a full zone leaves ≥ 10). Durability then depends on how fast we repair: the chance of losing 5 fragments of the same stripe before repair finishes. With ~1-2% annual disk failure and repair within hours, the probability is tiny, which is how providers reach '11 nines'. Costs of erasure coding: writes touch 14 nodes, small objects waste space (so small objects are packed together into stripes), and degraded reads need decoding, which costs CPU. A common pattern is to write new data replicated for speed and convert cold data to erasure coding in the background.
Metadata: the real scaling problem
The data path scales by adding disks, but every request touches metadata, and LIST needs sorted keys. The object index is a range-partitioned, ordered key-value store keyed by (bucket, key, version). Each range is a Raft group of 3-5 replicas, giving strong consistency: once a PUT's metadata commit is acknowledged, every later GET and LIST sees it. This is how S3 has offered strong read-after-write consistency since 2020. Range partitioning makes prefix LIST a scan of one or a few shards, but it creates hot spots when keys are sequential (logs/2026-10-25-00001…). Shards split automatically when they get too hot or large, and the router keeps a cached map of ranges to shards. Advising customers to add randomness at the start of keys is the old fix; automatic splitting is the modern one. A per-bucket write rate around 3,500 PUT/s per prefix is the type of limit you set while a shard splits. Deletes and overwrites are metadata changes first; data is freed later by garbage collection.
Multipart upload and large objects
A 5 TB object cannot be one HTTP request: a dropped connection would restart everything. Multipart upload splits it into parts of 5 MB to 5 GB (up to 10,000 parts). The client calls InitiateUpload and gets an upload_id, uploads parts in parallel (each part is stored as normal chunks with its own ETag), and retries only failed parts. CompleteUpload sends the ordered list of part numbers and ETags; the metadata service validates them and writes one object entry whose layout is the concatenation of the part layouts. No data is copied at completion, so it is fast. The final ETag is not a plain MD5 of the content but derived from the part MD5s, a detail worth knowing. Abandoned uploads keep using storage, so a lifecycle rule (abort incomplete uploads after N days) and GC clean them up. The same part structure makes parallel range GETs efficient for downloads.
Repair, scrubbing and garbage collection
At thousands of servers, failure is normal: several disks fail daily and bits rot silently. Each storage node verifies checksums continuously (scrubbing), reading every byte every few weeks. When a disk fails or a fragment is corrupt, the repair service finds affected stripes (through the placement map, not by scanning metadata), reads 10 healthy fragments, rebuilds the missing ones and writes them to new disks. Repair is prioritised by risk: a stripe with 3 missing fragments is fixed before one with 1 missing. Repair traffic is rate-limited so it does not hurt customer latency, but never so slow that durability drops. Garbage collection handles deletes and orphans: deleting an object removes the metadata entry and emits an event; GC later frees the fragments. Because fragments are inside append-only volume files, GC marks them dead and compacts volumes when the dead fraction passes a threshold (for example 30%), copying live data to new volumes.
8 Trade-offs
Erasure coding vs replication
Replication: simple, fast reads and writes, cheap repair, but 3x cost. Erasure coding: ~1.4x cost and higher durability, but more CPU, more network fan-out per write and expensive repair. Use replication for small and hot objects, erasure coding for large and cold data, or convert in the background.
Strong vs eventual consistency
Eventual consistency was historically easier (cached metadata, async index updates) but surprised users (read old data after write, LIST missing new keys). Strong consistency via Raft-replicated metadata adds a few ms per write and needs careful cache invalidation, but removes a whole class of client bugs. Modern designs choose strong.
Flat namespace vs real directories
Object stores use a flat key space; '/' is just a character and 'folders' are a LIST with delimiter. This makes sharding simple, but renaming a 'folder' means copying every object. A file system with real directories makes rename cheap but sharding hard. Say this when asked about rename.
9 Bottlenecks
Hot prefix in the metadata index
Sequential keys send all writes to the last range shard. Fix with automatic shard splitting by load, and by spreading new ranges quickly; for extreme cases, hash-prefix the key internally while keeping a secondary ordered index for LIST.
Tiny objects
Billions of 1 KB objects waste space with erasure coding and cost one metadata entry each. Pack small objects into larger stripes or store them replicated, and price per request so customers batch small files.
Hot object reads
One object read by thousands of clients overloads its 10 storage nodes. Front ends keep a short-lived cache of hot fragments, and a CDN in front of public content takes most of the load.
10 Failure handling
A whole availability zone fails
Fragments are spread so each zone holds at most 4-5 of 14; the remaining zones hold ≥ 10, so reads continue (with decoding). Metadata Raft groups have replicas in 3 zones and keep a majority. Writes continue using placements in the two remaining zones until the zone returns.
Storage node is slow, not dead
Slow nodes hurt tail latency. Readers request 1-2 extra fragments (or hedge after a short delay) and use the first 10 to arrive. Nodes with high latency are marked degraded and drained.
Metadata shard leader fails
Raft elects a new leader in a few seconds. In-flight writes either committed or not; clients retry PUTs safely because PUT of the same key is idempotent (last writer wins).
11 What impresses
- Explaining that durability comes from erasure coding plus fast repair, with a rough failure calculation
- Pointing out that metadata, not disk, is the hardest scaling problem
- Commit order: data fragments durable first, then metadata, so readers never see a broken object
- Hedged/extra fragment reads to beat slow nodes
- Small-object packing and volume compaction
- Knowing S3 is strongly consistent since 2020 and why that matters for data lakes
12 Mistakes
- Storing objects as files in a single big file system or as BLOBs in a SQL database
- Saying '3 replicas' with no cost discussion or failure-domain placement
- Ignoring metadata and LIST, or using a hash-partitioned store that cannot list by prefix
- Writing metadata before data is durable
- No plan for disk failures that happen every day
- Treating rename of a folder as a cheap operation
13 Follow-ups
Lessons this design uses
Practice questions
- Why do distributed databases usually keep 3 copies of each piece of data? Why not 2 or 5, and where should those copies live?
- A junior engineer stores product images as BLOB columns in the main Postgres database. The catalog has 20 million images. What problems will this cause and what would you do instead?
- Your app stores 2 PB of user-uploaded files in S3 Standard, and the storage bill is the biggest line item. Most files are never read after 30 days. How would you reduce cost safely?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.