Design S3 Storage
Designing an S3-like object storage system is a classic system design interview problem. It tests whether you can handle huge scale, strict durability requirements, and practical API/product constraints at the same time.
This walkthrough follows the Interview Framework and focuses on what you would present in a 45-60 minute interview.
Phase 1: Requirements
Functional Requirements
- Clients should be able to create buckets and manage bucket-level settings (versioning, lifecycle, ACL/policy).
- Clients should be able to upload, download, and delete objects by key.
- Clients should be able to upload large files with multipart upload and resume failed uploads.
- Clients should be able to list objects by prefix for folder-like browsing.
- Clients should be able to generate time-limited pre-signed URLs for delegated uploads/downloads.
Keep the MVP narrow in the interview: object CRUD, multipart upload, list, and access control. Mention features like event notifications and object lock as follow-ups.
Non-Functional Requirements
- Durability: 99.999999999% ("11 nines") for stored objects.
- Availability: 99.99%+ for read/write APIs.
- Scalability: Billions of objects and millions of requests per second globally.
- Latency: Low p99 for metadata operations, with streaming throughput for large objects.
- Consistency: Strong read-after-write and strong list consistency inside a region; cross-region replication can be asynchronous.
- Security: Strong IAM/policy enforcement, encryption in transit and at rest, and auditability.
State consistency clearly: "Within one region, writes are strongly consistent. Cross-region replication is eventually consistent unless explicitly configured for synchronous replication."
Capacity Estimation
Assume we are designing for a large cloud provider:
| Metric | Assumption |
|---|---|
| Active tenants | 50 million |
| New objects/day | 5 billion |
| Average object size | 5 MB |
| Daily ingest | 25 PB/day |
| Average write QPS | ~58K PUT/s |
| Peak write QPS | ~600K PUT/s (10x burst) |
| Peak read QPS | ~6 million GET/HEAD/s |
Estimated annual new logical storage:
25 PB/day x 365 ~= 9.1 EB/yearlogical data- With erasure coding/replication overhead (say
1.4xeffective):~12.7 EB/yearphysical footprint
In an interview, exact numbers matter less than showing order-of-magnitude reasoning and using that to justify architecture choices (partitioning, erasure coding, lifecycle tiering).
Phase 2: Data Model
Core Entities
Bucket(
bucket_id UUID PK,
tenant_id UUID,
name VARCHAR UNIQUE,
region VARCHAR,
versioning_status ENUM('enabled','suspended'),
default_encryption JSONB,
created_at TIMESTAMP
)
Object(
object_id UUID PK,
bucket_id UUID FK,
object_key TEXT, -- e.g. "images/2026/hero.png"
latest_version_id UUID,
object_state ENUM('active','delete_marker'),
created_at TIMESTAMP,
updated_at TIMESTAMP,
UNIQUE(bucket_id, object_key)
)
ObjectVersion(
version_id UUID PK,
object_id UUID FK,
content_hash CHAR(64),
size_bytes BIGINT,
storage_class ENUM('standard','infrequent','archive'),
etag VARCHAR,
metadata JSONB,
chunk_manifest JSONB, -- ordered chunk IDs + checksums
retention_until TIMESTAMP NULL,
created_at TIMESTAMP
)
MultipartUpload(
upload_id UUID PK,
bucket_id UUID FK,
object_key TEXT,
initiated_by UUID,
status ENUM('initiated','completed','aborted'),
created_at TIMESTAMP,
expires_at TIMESTAMP
)
MultipartPart(
upload_id UUID FK,
part_number INT,
chunk_ref TEXT,
size_bytes BIGINT,
part_etag VARCHAR,
PRIMARY KEY(upload_id, part_number)
)
Relationships
- One Bucket has many objects.
- One Object has one current pointer plus many ObjectVersion rows.
- One MultipartUpload has many MultipartPart rows before completion.
- ObjectVersion points to immutable chunk manifests stored in data nodes.
Call out that metadata and blob data are separated: metadata in a strongly consistent store, object bytes in a distributed chunk store.
Phase 3: API Design
Protocol Choice
- External API: REST over HTTPS (matches S3-style semantics and broad client compatibility).
- Internal service-to-service: gRPC for low-latency, typed contracts between metadata, placement, and storage-node services.
Core Endpoints
PUT /{bucket}
Create bucket
PUT /{bucket}/{key}
Upload object (single-part)
Headers: Content-Length, Content-MD5, x-amz-meta-*
GET /{bucket}/{key}
Download object (supports Range)
HEAD /{bucket}/{key}
Fetch metadata only (size, etag, version)
DELETE /{bucket}/{key}
Delete object (or create delete marker when versioning enabled)
GET /{bucket}?list-type=2&prefix=...&continuation-token=...
List objects by prefix with pagination
Multipart Upload APIs
POST /{bucket}/{key}?uploads
Initiate multipart upload
Response: { uploadId }
PUT /{bucket}/{key}?partNumber={n}&uploadId={id}
Upload one part
Response header: ETag
POST /{bucket}/{key}?uploadId={id}
Complete upload with ordered part list + ETags
DELETE /{bucket}/{key}?uploadId={id}
Abort multipart upload
Access Control APIs
PUT /{bucket}?policy
Set bucket policy JSON
POST /v1/presign-url
Generate pre-signed URL
Request: { bucket, key, method, expires_in_seconds }
Never skip idempotency on write paths. Retries are common; clients should be able to safely retry PUT or multipart completion without creating duplicate versions.
Phase 4: High-Level Design
Architecture Overview
Write Flow (Single-Part Upload)
- Client sends
PUT /bucket/keywith auth signature and checksum. - API gateway authenticates and authorizes against bucket policy.
- Metadata service resolves bucket and asks placement service for chunk placement (multi-AZ).
- Data proxy streams object bytes to selected storage nodes.
- Storage nodes persist chunks and acknowledge durability threshold (for example, erasure set completion).
- Metadata service commits new
ObjectVersionatomically and emits an event. - Client receives success with
ETagandversionId.
Read Flow
- Client calls
GET /bucket/key(optionally withRange). - Metadata service resolves the latest version and chunk manifest.
- Data proxy reads chunks from nearest healthy nodes.
- Response is streamed to client with integrity headers.
Commit metadata only after durable chunk persistence. If metadata commits first and data write fails, you create dangling pointers and data loss risk.
Multipart Upload Flow
Key Component Decisions
- Cell-based architecture: isolate blast radius; each cell owns a shard of buckets/objects.
- Metadata-data separation: strongly consistent metadata store + distributed immutable chunk store.
- Erasure coding for warm/cold data: lower storage cost than full replication while preserving durability.
- Background pipelines: lifecycle transitions, replication, inventory scans, and repair are asynchronous.
Phase 5: Scaling & Trade-offs
How the Design Meets NFRs
- Durability
- Multi-AZ placement for each object version.
- Checksums per chunk + periodic scrubbing and repair.
- Immutable versions simplify recovery and corruption rollback.
- Availability
- Cell isolation limits outage impact.
- Stateless API/data-proxy layer scales horizontally.
- Fast failover to healthy nodes on read path.
- Low latency
- Metadata cached at router and metadata-service layer.
- Direct streaming from data proxy to storage nodes.
- Range GET support avoids reading full large objects.
- Scale
- Partition metadata by
(bucket_id, key_prefix_hash). - Auto-splitting hot partitions and adaptive load balancing.
- Partition metadata by
- Security
- Signed requests validated at API gateway and policy engine.
- Encryption in transit (TLS) and at rest (KMS-managed keys).
- Access/audit logs for all control-plane and data-plane operations.
Bottlenecks and Mitigations
| Bottleneck | Why It Hurts | Mitigation |
|---|---|---|
| Metadata hot keys | Popular prefixes overload partitions | Prefix hash sharding + adaptive splits |
| Small object overhead | Metadata dominates storage/IOPS | Object packing and minimum billable size strategy |
| Multipart abandoned uploads | Leaks storage | TTL-based cleanup and abort sweeps |
| Cross-region replication lag | DR recovery point grows | Priority queues + per-bucket replication SLOs |
Trade-offs
| Decision | Option A | Option B | Chosen Approach |
|---|---|---|---|
| Durability format | 3x replication | Erasure coding | Hybrid (replication for hot writes, EC for warm/cold) |
| Consistency model | Strong global | Regional strong + global eventual | Regional strong + async cross-region replication |
| Write path | Sync multi-region commit | Sync in-region + async global | Sync in-region for low latency and high availability |
| Metadata store | SQL-only | Distributed KV + secondary index | Distributed KV for scale + index service for list/query |
Deep Dive: Metadata Partitioning
- Start with partition key:
bucket_id + key_hash_prefix. - Track per-partition QPS and storage growth.
- Split partitions when thresholds exceed SLO.
- Use consistent hashing for placement but keep logical mapping in metadata control plane.
Interviewers often ask "What breaks first?" For S3-like systems, metadata partitions and repair bandwidth are common first-order constraints, not raw disk capacity.
Deep Dive: Data Integrity and Repair
- Store content hash and per-chunk checksums.
- Run continuous scrubbing jobs to detect silent corruption.
- If corruption is detected, reconstruct via replicas/parity shards and rewrite healthy chunks.
- Keep repair traffic rate-limited so it does not starve customer reads/writes.
This is a strong senior-level point: durability is not just replication at write time; it is continuous verification plus repair.
Common Pitfalls
Treating object storage like a file system: Object stores do not support in-place updates or POSIX semantics well. Design around immutable versions and overwrite-as-new-version.
Ignoring list-object scalability: Listing by prefix can become a metadata hotspot. You need pagination, indexed prefixes, and partition-aware listing.
No idempotency strategy: Network retries without idempotency cause duplicate versions and inconsistent client behavior.
Skipping lifecycle and cleanup: Abandoned multipart uploads and stale versions silently create huge cost growth.
Claiming strong consistency everywhere: Strong consistency across regions has major latency and availability costs. Be explicit about consistency boundaries.
Interview Checklist
Before concluding, verify you covered:
- Functional requirements: bucket/object CRUD, list, multipart, auth
- Durability target and how architecture achieves it (multi-AZ, checksums, repair)
- Data model separation: metadata vs object bytes
- API surface including multipart flow and idempotency
- End-to-end write and read data flows
- Scaling plan for metadata hotspots and small-object overhead
- Trade-offs around consistency, latency, and cost
- Failure handling (node loss, AZ loss, replication lag)
Summary
| Aspect | Decision | Rationale |
|---|---|---|
| Storage model | Immutable object versions | Simplifies concurrency, rollback, and durability |
| Metadata | Strongly consistent distributed metadata store | Correct read-after-write and list semantics |
| Object data | Chunked storage nodes across AZs | High throughput and fault tolerance |
| Large uploads | Multipart API with part manifests | Resume support and parallelism |
| Durability | Multi-AZ + checksum scrub + repair | Achieve "11 nines" durability in practice |
| Cost control | Lifecycle tiers + erasure coding | Reduce long-term storage cost |
| Global strategy | Regional strong consistency + async replication | Balance latency, availability, and DR |
If you present this structure clearly in an interview, you demonstrate both practical architecture skills and strong trade-off reasoning expected at senior levels.