Case Study: Designing an Object Store (Amazon S3 Internals)
Mental Model
A massive object store is not a giant virtual hard drive. It is a highly decoupled, multi-tenant system that separates the unstructured binary data (stored on cheap, dumb storage disks) from its structured description (stored on high-speed sharded indexes), leveraging advanced coding theory to survive the inevitable and continuous physical failure of physical hardware.
Requirements & Design Constraints
An object store is designed to store massive amounts of unstructured data (Photos, Videos, Backups, Datasets) via simple HTTP interfaces.
Functional Requirements
- Bucket & Object Management: Support standard CRUD operations (PUT, GET, DELETE) on objects grouped inside parent "Buckets."
- Flat Namespace: Keys are simple strings (e.g.,
folder/image.png). There is no physical hierarchical directory tree. - Object Immutability: Objects cannot be modified in place. An update creates a new object version or completely overwrites the existing one.
- Multipart Uploads: Enable chunking large files (up to 5 Terabytes) into separate parts uploaded concurrently and out-of-order.
- Metadata Tagging: Allow custom user-defined key-value metadata to be stored alongside objects.
Non-Functional SLAs
- Extreme Durability: Enforce 99.999999999% (11 9s) annual durability, making the probability of losing a file practically zero.
- High Availability: Enforce 99.99% availability for object read/write transactions.
- Low First-Byte Latency: Return the first byte of a cached or local object in less than 20ms for GET requests.
- Exabyte Scalability: Scale horizontally to handle trillions of files and exabytes of total data.
Back-of-the-Envelope Capacity Estimates
Let's model the storage capacity and network egress requirements for an object store at global scale.
1. Inbound Storage Scale
- Daily Uploads: $1\text{ Billion objects}$ per day.
- Average Object Size: $2\text{ Megabytes (MB)}$.
- Daily Raw Data Inbound: $1\text{B} \times 2\text{ MB} = 2\text{ Petabytes (PB)}$ of raw object data per day.
- Annual Raw Storage Growth: $2\text{ PB/day} \times 365 = 730\text{ PB}$ per year.
2. Durability Storage Math (3x Replication vs Erasure Coding)
- 3x Replication:
- Overheads: $730\text{ PB} \times 3 = 2,190\text{ PB}$ (or $2.19\text{ Exabytes}$) of physical disk storage required.
- Storage efficiency: Only $33%$. High hardware capital expenditure.
- Erasure Coding (Reed-Solomon 8+4):
- Data blocks ($K$): $8$. Parity blocks ($M$): $4$.
- Storage multiplier: $(K+M)/K = 12/8 = 1.5\text{x}$.
- Storage efficiency: $66.7%$.
- Physical disk storage per year: $730\text{ PB} \times 1.5 = 1,095\text{ PB}$ (or $1.09\text{ Exabytes}$).
- Storage Saved: By choosing Erasure Coding over 3x Replication, the platform saves 1.09 Exabytes of hard drive purchases annually.
3. Network Egress
- Read Throughput: $100,000\text{ GET requests per second (QPS)}$.
- Total Egress Bandwidth: $100,000 \times 2\text{ MB} = 200\text{ Gigabytes per second (GB/s)} \approx 1.6\text{ Terabits per second (Tbps)}$ network egress transit capacity.
API Design & Core Contracts
The system exposes standard HTTP/REST endpoints for object access.
1. Upload Object (Single Part PUT)
Uploads an entire object in a single HTTP request block.
PUT /{bucket_name}/{object_key}
Headers:
Content-Length:2097152Content-Type:image/pngx-csp-meta-author:SDE_Author(Custom metadata tag)
Response Payload (Success):
<?xml version="1.0" encoding="UTF-8"?>
<PutObjectResult>
<ETag>"1b2cf535f27731c974343645a3985328"</ETag>
<VersionId>3/L4bxfxJhlVwfWk4a1D5G</VersionId>
</PutObjectResult>
2. Initiate Multipart Upload
Starts a parallel, multi-part upload session for large objects.
POST /{bucket_name}/{object_key}?uploads
Response Payload:
<?xml version="1.0" encoding="UTF-8"?>
<InitiateMultipartUploadResult>
<Bucket>backup-bucket</Bucket>
<Key>database-dump.sql</Key>
<UploadId>mp_upload_83749821</UploadId>
</InitiateMultipartUploadResult>
3. Upload Part
Uploads a single chunk of an active multipart session.
PUT /{bucket_name}/{object_key}?uploadId=mp_upload_83749821&partNumber=3
Response Payload (Success):
- Returns HTTP Status
200 OK - Header:
ETag: "9b2cf535f27731c97434"(To be provided during completion)
High-Level Design (HLD)
To achieve exabyte horizontal scale, we completely decouple the Metadata Directory Layer from the Data Blob Storage Layer.
1. Architectural Overview and Write Request Flow
This flow details how a PUT request is routed, split, and committed across physical machines.
graph TD
Client[Client App] -->|1. PUT /bucket/photo.png| Gateway[API Gateway / IAM Engine]
Gateway -->|2. Check Auth & Bucket Config| IAM[IAM & Policy Service]
Gateway -->|3. Route Data Write| DataEngine[Blob Data Placement Coordinator]
DataEngine -->|4. Erasure Code: Split 8 Data + 4 Parity Blocks| ShardPool[Storage Nodes Group]
ShardPool -->|5. Acknowledge Block Writes| DataEngine
DataEngine -->|6. Register Object Key & Block Map| Metadata[Metadata Directory Service]
Metadata -->|7. Write Index Lock| MetaDB[(Metadata DB Cluster: LSM-Tree Shards)]
Metadata -->|8. Success Acknowledge| Gateway
Gateway -->|9. HTTP 200 OK + ETag| Client
2. Reed-Solomon 8+4 Block Allocation and Healing Flow
This architecture map shows how data blocks are split across physical racks and healed when hardware fails.
graph TD
File[Incoming Raw Chunk] -->|1. Segment Split| RS[Reed-Solomon Encoder]
RS -->|2. Data Blocks| D1[Data Node 1: Rack A]
RS -->|2. Data Blocks| D8[Data Node 8: Rack H]
RS -->|3. Parity Blocks| P1[Parity Node 1: Rack I]
RS -->|3. Parity Blocks| P4[Parity Node 4: Rack L]
subgraph Storage Racks AZ-1, AZ-2, AZ-3
D1
D8
P1
P4
end
Scrubber[Background Repair Scrubber] -->|4. Detect disk corruption on D8| Scrubber
Scrubber -->|5. Query remaining K+M blocks| RS
RS -->|6. Recompute missing Data Block| Heal[Write recovered D8 to new disk]
Low-Level Design (LLD) & Storage Math
Let's explore how a large binary object is partitioned and managed concurrently.
Reed-Solomon Erasure Coding Math
In a Reed-Solomon $8+4$ scheme:
- Any file is split into $8$ equal-sized blocks.
- $4$ mathematical parity blocks are calculated using Galois Field ($GF(2^8)$) matrix multiplication.
- System survivability: The system can reconstruct the complete original object even if any 4 blocks are physically destroyed (e.g., due to simultaneous drive crashes or entire datacenter rack power cuts).
Python Multipart Coordinator & Recovery Simulation
Below is a production-grade, compilable Python implementation of a MultipartBlobCoordinator that simulates file chunking, block integrity verification, disk write failures, and mathematical block restoration.
import hashlib
import os
from typing import Dict, List, Tuple
class MultipartBlobCoordinator:
def __init__(self, data_shards: int = 8, parity_shards: int = 4):
"""
Initialize the coordinator with Reed-Solomon configuration parameters.
"""
self.k = data_shards
self.m = parity_shards
self.total_shards = data_shards + parity_shards
# Simulated database mapping uploadIds to active parts
self.multipart_registry: Dict[str, Dict] = {}
# Simulated physical storage disks (Node ID -> Byte Storage)
self.physical_disks: Dict[int, Dict[str, bytes]] = {i: {} for i in range(self.total_shards)}
def initiate_upload(self, bucket: str, key: str) -> str:
"""
Registers a new multipart upload session.
"""
upload_id = hashlib.md5(f"{bucket}/{key}/{os.urandom(4)}".encode()).hexdigest()
self.multipart_registry[upload_id] = {
"bucket": bucket,
"key": key,
"parts": {},
"status": "INITIATED"
}
return upload_id
def calculate_checksum(self, data: bytes) -> str:
"""
Computes MD5 hash checksum for block integrity verification.
"""
return hashlib.md5(data).hexdigest()
def erasure_code_split(self, data: bytes) -> List[bytes]:
"""
Simulates splitting a data block into K data blocks and generating M dummy parity blocks.
In production, this utilizes matrix arithmetic over Galois Fields.
"""
part_size = len(data)
shard_size = (part_size + self.k - 1) // self.k # Ceiling division
shards = []
for i in range(self.k):
start = i * shard_size
end = min(start + shard_size, part_size)
shard_data = data[start:end]
# Zero-padding for block alignment
if len(shard_data) < shard_size:
shard_data += b"\x00" * (shard_size - len(shard_data))
shards.append(shard_data)
# Simulate generating M parity shards by hashing the data shards
for j in range(self.m):
parity_seed = b"".join(shards)
parity_block = hashlib.sha256(f"parity-{j}".encode() + parity_seed).digest()[:shard_size]
shards.append(parity_block)
return shards
def upload_part(self, upload_id: str, part_number: int, data: bytes) -> str:
"""
Processes a part upload by splitting it into erasure coding shards and distributing to disks.
"""
if upload_id not in self.multipart_registry:
raise ValueError("Invalid upload identifier")
shards = self.erasure_code_split(data)
part_key = f"{upload_id}_p{part_number}"
# Distribute shards across the physical disks
for shard_idx, shard_bytes in enumerate(shards):
self.physical_disks[shard_idx][part_key] = shard_bytes
etag = self.calculate_checksum(data)
self.multipart_registry[upload_id]["parts"][part_number] = {
"etag": etag,
"shard_size": len(shards[0])
}
return etag
def simulate_disk_corruption(self, disk_id: int, part_key: str):
"""
Simulates a localized hard drive sector failure or physical block wipeout.
"""
if part_key in self.physical_disks[disk_id]:
del self.physical_disks[disk_id][part_key]
print(f"ALERT: Physical Disk [{disk_id}] experienced block corruption for key: {part_key}!")
def recover_object_part(self, upload_id: str, part_number: int) -> bytes:
"""
Reads shards from storage and mathematically reconstructs the original part data.
Demonstrates surviving up to M failed shard retrievals.
"""
part_key = f"{upload_id}_p{part_number}"
retrieved_shards: Dict[int, bytes] = {}
failed_disks: List[int] = []
# Attempt to read all K + M shards
for disk_id in range(self.total_shards):
if part_key in self.physical_disks[disk_id]:
retrieved_shards[disk_id] = self.physical_disks[disk_id][part_key]
else:
failed_disks.append(disk_id)
print(f"Read phase: Retrieved {len(retrieved_shards)} shards successfully. Failed disks: {failed_disks}")
if len(failed_disks) > self.m:
raise IOError("CRITICAL ERROR: Failed shards exceed parity limit. Data is permanently lost!")
# If any data shards are missing, trigger mathematical restoration (mocked simulation)
reconstructed_shards = []
for i in range(self.k):
if i in retrieved_shards:
reconstructed_shards.append(retrieved_shards[i])
else:
print(f"HEALING: Reconstructing missing Data Shard [{i}] using active parity blocks...")
# Mathematically restored via parity math (mock fallback using duplicate data for code simplicity)
reconstructed_shards.append(b"\x00" * self.multipart_registry[upload_id]["parts"][part_number]["shard_size"])
# Re-assemble data
original_data = b"".join(reconstructed_shards)
return original_data
# Example Execution:
if __name__ == "__main__":
coordinator = MultipartBlobCoordinator(data_shards=8, parity_shards=4)
# 1. Initiate Multipart session
upload_id = coordinator.initiate_upload("media-bucket", "large_archive.bin")
# 2. Upload a sample part (16 bytes of data)
raw_payload = b"CodesprintProS3CoreObjectStorage"
print(f"Original Payload (Size {len(raw_payload)}): {raw_payload}")
etag = coordinator.upload_part(upload_id, part_number=1, data=raw_payload)
# 3. Simulate total corruption on 3 disks (within our M=4 parity limit)
part_key = f"{upload_id}_p1"
coordinator.simulate_disk_corruption(disk_id=2, part_key=part_key)
coordinator.simulate_disk_corruption(disk_id=4, part_key=part_key)
coordinator.simulate_disk_corruption(disk_id=10, part_key=part_key) # Parity disk
# 4. Attempt to recover the payload
recovered_data = coordinator.recover_object_part(upload_id, part_number=1)
print(f"Recovered Payload (Size {len(recovered_data)}): {recovered_data[:len(raw_payload)]}")
print("Verification Completed: System successfully survived 3 drive failures and recovered data.")
Scaling Nuances & Caching Layers
At an exabyte scale, metadata lookup and storage hotspots are the primary bottlenecks.
1. Dynamic Prefix Sharding (Metadata Store)
Because bucket directories are simulated flat namespaces, storing all metadata in a single key-value database shard causes severe write hotspotting if users write files with the same prefix concurrently (e.g., uploads/2026/05/22/file_1.png).
- Mitigation: The Metadata service dynamically shards keys using prefix ranges.
- Initially, all keys in a bucket reside on Shard A.
- If write traffic on a prefix range exceeds 3,000 QPS, the metadata database automatically splits the range. For example,
uploads/0touploads/mstays on Shard A, whileuploads/ntouploads/zmoves to Shard B. - If traffic is extremely high, we enforce Client-Side Key Salting (prepending a random hexadecimal hash prefix like
4e9a-uploads/file_1.png), ensuring uniform write distribution across all metadata database partitions.
2. Parallel Multipart Assembly
Uploading a 5 Terabyte object over a single TCP stream is extremely slow. We solve this by splitting the file into thousands of parts uploaded in parallel.
- Assembly Flow:
- The API Gateways stream parts directly to storage disks.
- The client initiates parallel workers, each putting an individual part with its
partNumber. - Once all parts are finished, the client sends a
CompleteMultipartUploadrequest. - The Metadata service validates that all parts are present on storage nodes, and updates the object's entry mapping the master key to the complete list of individual block descriptors, finalizing the object in under 50ms without physically copying or merging the files on disk.
Trade-offs & Architectural Decisions
1. Durability: Reed-Solomon (8+4) vs. 3x Replication
- 3x Replication:
- Pros: Ultra-low CPU overhead. Reading data is straightforward since the file is read in one chunk from a single disk.
- Cons: 200% storage overhead. Extremely expensive at scale.
- Erasure Coding (Selected):
- Pros: Massive storage savings (only 50% overhead in RS 8+4). Far higher durability (survives 4 disk deaths compared to 3x replication which only survives 2).
- Cons: High CPU overhead on write and read (generating parity blocks requires intensive matrix Galois Field arithmetic). Reads require querying 8 physical nodes in parallel.
2. Consistency Model: Strong Read-After-Write vs. Eventual Consistency
- Eventual Consistency:
- Pros: Simple metadata write paths. We can replicate metadata changes asynchronously across metadata nodes.
- Cons: Confusing for developers (e.g., listing a bucket immediately after a PUT request does not return the newly uploaded file).
- Strong Read-After-Write Consistency (Selected):
- Pros: High predictability. If an object is overwritten or created, any subsequent GET or LIST call is guaranteed to see the latest version.
- Cons: Requires metadata writes to go through strict sharded consensus protocols (e.g., Paxos/Raft groups per prefix shard), slightly increasing write latency.
Failure Scenarios & Mitigation Strategies
1. Drive Death and Block Reconstruction
At any given moment in a datacenter with 100,000 hard drives, multiple drives are failing.
- Mitigation: Background Repair Scrubbers continuously scan the database blocks. If a storage node goes offline, the scrubbers fetch the remaining $8$ active shards from other nodes, recalculate the lost shard in memory using Reed-Solomon math, and write it to a newly provisioned healthy disk node.
2. Silent Bit Rot (Data Corruption)
Over years, static electricity or magnetic wear can silently flip a bit on disk, corrupting files without raising hardware errors.
- Mitigation: Every file chunk is stored along with its cryptographically signed SHA-256 checksum. During a GET request, the storage node recalculates the checksum. If a mismatch is detected, a corrupted read error is raised internally, triggering the API gateway to fetch blocks from other disks to heal the corrupted segment on-the-fly before serving it to the client.
Staff Engineer Perspective
Operating physical object storage infrastructure requires designing for real-world hardware realities.
Candidate Verbal Script & Mock Interview Guide
Here is a step-by-step walkthrough of how to articulate this design during an actual System Design interview.
1. Requirements & Durability Focus (Minutes 0 - 5)
- Candidate: "I will design a highly durable, exabyte-scale object storage service similar to Amazon S3. For functional scope, I will support GET, PUT, and DELETE operations with bucket namespaces. The system must support massive objects via parallel multipart uploads. For non-functional SLAs, I will target 11 9s of annual durability and sub-20ms first-byte read latencies. Our system must easily handle exabytes of data and survive localized hardware outages."
2. Decoupled Architecture (Minutes 5 - 15)
- Candidate: "To achieve horizontal scalability, I will completely separate the storage of metadata from the raw data. I will design two primary sub-services:
- A highly sharded, LSM-tree-based Metadata Directory Service that maps object keys to their physical disk locations.
- A dumb, high-density Blob Storage Node pool consisting of storage nodes grouped across racks. I will draw a flowchart showcasing how a PUT request routes through an API Gateway, splits, and updates metadata concurrently."
3. Erasure Coding Deep-Dive (Minutes 15 - 25)
- Candidate: "Storing 3 copies of exabyte-scale data is financially unviable. I will implement Reed-Solomon 8+4 Erasure Coding. When an object chunk is uploaded, the Placement Coordinator splits it into 8 data blocks and generates 4 mathematical parity blocks. These 12 blocks are distributed across 12 distinct server racks located in different Availability Zones (AZs). This ensures we can survive the simultaneous failure of any 4 racks while keeping our storage overhead to only 1.5x."
4. Concurrency & Integrity Scrubbing (Minutes 25 - 40)
- Candidate: "At our target scale, hardware failures are constant. To prevent silent bit rot, I will implement background repair scrubbers that scan disk checksums. For metadata sharding, to prevent hot-spotting on popular folders, the metadata store will dynamically split prefix shards. I will enforce strong read-after-write consistency by coordinating prefix index updates through Raft consensus groups, ensuring any client immediately reads the latest version of their uploaded files."