Skip to content
advancedPhase 52 · HLD Case Studies

File Storage System

Design a distributed file storage system like S3.

1h 30m
0 problems
Topic Progress0%

Requirements & Architecture

Functional Requirements

  1. Upload/Download Files: Support objects from 1 byte to 5 TB.
  2. Multi-part Upload: Split large files into chunks for parallel upload and resumability.
  3. Versioning: Maintain multiple versions of an object; support rollback.
  4. Access Control: Bucket-level and object-level ACLs, IAM policies.
  5. Lifecycle Policies: Auto-transition objects between storage tiers (hot → warm → cold).
  6. MFA Delete: Require multi-factor authentication for permanent deletion.
  7. Object Lock: WORM (Write Once Read Many) compliance for regulatory requirements.
  8. Cross-Region Replication: Async replication to a secondary region.

Non-Functional Requirements

Requirement Target
Durability 99.999999999% (11 nines)
Availability 99.99%
Throughput 100K+ concurrent uploads/downloads
Object Size 1 byte – 5 TB
Latency (first byte) < 100ms for hot data
Scalability Trillions of objects, exabytes of data
Cost Efficiency Pay-per-use, tiered storage

High-Level Architecture

┌──────────┐     ┌──────────────┐     ┌──────────────────┐
│  Client   │────▶│ API Gateway  │────▶│  Metadata Service │
│ (SDK/CLI/ │     │ (Auth, Rate  │     │  (Object Index)   │
│  Console) │     │  Limiting)   │     │  MySQL/etcd       │
└──────────┘     └──────────────┘     └────────┬─────────┘
                                                │
                              ┌─────────────────┼─────────────────┐
                              ▼                 ▼                 ▼
                       ┌──────────┐     ┌──────────┐     ┌──────────┐
                       │ Data Node│     │ Data Node│     │ Data Node│
                       │  (Tier 1)│     │  (Tier 2)│     │  (Tier 3)│
                       │  Hot/SSD │     │  Warm/HDD│     │  Cold/   │
                       │          │     │          │     │  Glacier │
                       └────┬─────┘     └────┬─────┘     └────┬─────┘
                            │                 │                 │
                            ▼                 ▼                 ▼
                       ┌──────────┐     ┌──────────┐     ┌──────────┐
                       │ Storage  │     │ Storage  │     │ Storage  │
                       │ Disks    │     │ Disks    │     │ Tape/Vault│
                       └──────────┘     └──────────┘     └──────────┘

Data Model

Account
  └── Bucket
        ├── Object (key)
        │     ├── Version (version_id)
        │     │     ├── Chunk 1 (chunk_id, checksum, data_node)
        │     │     ├── Chunk 2 (chunk_id, checksum, data_node)
        │     │     └── Chunk N
        │     ├── ACL (owner, permissions)
        │     ├── Metadata (content_type, encoding, tags)
        │     └── Lifecycle (tier, expiration, transition_date)
        └── Bucket Policy

Schema:

-- Metadata tables
CREATE TABLE buckets (
    bucket_id     BIGINT PRIMARY KEY,
    account_id    BIGINT NOT NULL,
    name          VARCHAR(63) UNIQUE,
    region        VARCHAR(16),
    versioning    ENUM('enabled', 'suspended', 'disabled'),
    created_at    TIMESTAMP,
    lifecycle     JSON  -- tiering rules
);

CREATE TABLE objects (
    object_id     BIGINT PRIMARY KEY,
    bucket_id     BIGINT,
    key           VARCHAR(1024),  -- S3 allows 1024-byte keys
    version_id    BIGINT,
    is_latest     BOOLEAN DEFAULT TRUE,
    size_bytes    BIGINT,
    etag          VARCHAR(32),
    content_type  VARCHAR(128),
    checksum      VARCHAR(64),  -- SHA-256
    storage_class ENUM('STANDARD', 'REDUCED_REDUNDANCY', 'STANDARD_IA', 'ONEZONE_IA', 'GLACIER', 'DEEP_ARCHIVE'),
    created_at    TIMESTAMP,
    deleted_at    TIMESTAMP NULL,
    UNIQUE KEY (bucket_id, key, version_id)
);

CREATE TABLE chunks (
    chunk_id      BIGINT PRIMARY KEY,
    object_id     BIGINT,
    version_id    BIGINT,
    chunk_index   INT,
    size_bytes    BIGINT,
    checksum      VARCHAR(64),
    data_node_id  INT,
    storage_path  VARCHAR(512),
    replicas      JSON,  -- [{node_id, path, checksum}]
    INDEX (object_id, version_id)
);

API Design

Operation Method Description
PutObject PUT Upload an object (up to 5GB single PUT)
GetObject GET Download an object
DeleteObject DELETE Soft-delete (marks as deleted)
HeadObject HEAD Get object metadata without body
ListObjects GET List objects in a bucket (paginated)
InitiateMultipartUpload POST Start multi-part upload
UploadPart PUT Upload a chunk of a multi-part upload
CompleteMultipartUpload POST Finalize multi-part upload
AbortMultipartUpload DELETE Cancel incomplete multi-part upload
CopyObject PUT Server-side copy
GetObjectAttributes GET Get checksum, storage class, etc.

Upload Flow (Multi-part)

1. Client                    API Gateway              Metadata Service
  │                             │                          │
  │──InitiateMultipartUpload──▶│                          │
  │                             │──Create Upload Entry───▶│
  │                             │◀──upload_id─────────────│
  │◀──upload_id────────────────│                          │
  │                             │                          │
  │──UploadPart (chunk 1)─────▶│                          │
  │                             │──Store chunk────────────▶│
  │◀──part ETag────────────────│                          │
  │                             │                          │
  │──UploadPart (chunk 2)─────▶│  (parallel uploads)      │
  │                             │                          │
  │──CompleteMultipartUpload──▶│                          │
  │                             │──Create Object Entry───▶│
  │                             │──Mark chunks as live────▶│
  │◀──200 OK + ETag────────────│                          │

Chunk size selection:

Object Size Recommended Chunk
< 100 MB Single PUT (no multipart)
100 MB – 1 GB 8 MB chunks
1 GB – 10 GB 16 MB chunks
10 GB – 100 GB 32 MB chunks
> 100 GB 64 MB chunks (max 10,000 parts)

Download Flow

1. Client                    API Gateway              Metadata Service        Data Nodes
  │                             │                          │                      │
  │──GetObject─────────────────▶│                          │                      │
  │                             │──Lookup object──────────▶│                      │
  │                             │◀──chunk locations────────│                      │
  │                             │                          │                      │
  │                             │──Fetch chunks───────────▶│ (parallel)           │
  │                             │◀──chunk data─────────────│                      │
  │◀──stream response──────────│                          │                      │

Optimizations:

  • Range requests: Support Range: bytes=0-1023 for partial downloads.
  • Parallel chunk download: Fetch non-dependent chunks in parallel.
  • CDN integration: Cache hot objects at edge locations.
  • Byte-range fetching: Client can request specific byte ranges for large objects.

Storage Backend & Consistency

Storage Tiers

Tier Media Access Time Durability Cost/GB/month Use Case
Hot (Standard) SSD < 10ms 11 nines $0.023 Frequently accessed
Warm (Standard-IA) HDD < 50ms 11 nines $0.0125 Infrequent access
Cold (Glacier) Tape/Vault minutes–hours 11 nines $0.004 Archive/compliance
Deep Archive Tape hours 11 nines $0.00099 Long-term archive

Lifecycle Policy Engine

class LifecyclePolicy:
    def apply(self, object):
        age_days = (now() - object.created_at).days
        
        # Transition rules (evaluated in order)
        for rule in sorted(self.rules, key=lambda r: r.days):
            if age_days >= rule.days and object.storage_class != rule.target_class:
                self.transition(object, rule.target_class)
                break
        
        # Expiration rules
        if self.expiration_days and age_days >= self.expiration_days:
            self.delete(object)

Example lifecycle configuration:

{
  "rules": [
    {
      "id": "transition-to-warm",
      "prefix": "logs/",
      "transition": {
        "days": 30,
        "storage_class": "STANDARD_IA"
      }
    },
    {
      "id": "transition-to-cold",
      "prefix": "logs/",
      "transition": {
        "days": 90,
        "storage_class": "GLACIER"
      }
    },
    {
      "id": "expire-old-logs",
      "prefix": "logs/",
      "expiration": {
        "days": 365
      }
    }
  ]
}

Consistency Model

Operation Consistency
PUT new object Strongly consistent (read-after-write)
GET existing object Strongly consistent
DELETE object Strongly consistent
LIST objects Eventually consistent (may lag behind writes)
Overwrite existing object Strongly consistent

Why eventual consistency for LIST?

  • LIST operations scan large metadata indexes.
  • Maintaining strong consistency for LIST would require distributed transactions across all metadata partitions, which is expensive.
  • S3 now provides strong consistency for LIST (since Dec 2020), but this requires careful metadata service design.

Metadata Service Design for Strong Consistency:

┌──────────────────────────────────────────────────┐
│              Metadata Service                     │
│                                                   │
│  ┌─────────────┐    ┌─────────────┐             │
│  │  Primary     │───▶│  Replica 1  │             │
│  │  (Leader)    │    │  (Follower) │             │
│  └──────┬──────┘    └─────────────┘             │
│         │                                        │
│         ▼                                        │
│  ┌─────────────┐    ┌─────────────┐             │
│  │  Replica 2  │    │  Replica 3  │             │
│  │  (Follower) │    │  (Follower) │             │
│  └─────────────┘    └─────────────┘             │
│                                                   │
│  Consensus: Raft or Multi-Paxos                  │
│  Replication: Synchronous (leader → majority)    │
└──────────────────────────────────────────────────┘

Write path (strong consistency):

  1. Client sends PUT to metadata service.
  2. Leader appends to write-ahead log (WAL).
  3. Leader replicates to majority (2 of 3 followers).
  4. Leader commits and responds to client.
  5. Followers apply committed entries asynchronously.

Read path (strong consistency):

  1. Client sends GET to metadata service.
  2. Read from leader (or follower that is caught up).
  3. Return latest committed version.

Versioning Implementation

Object: "photos/image.jpg"
  ├── version "3939393939" (latest)
  │     ├── chunk_001 → data_node_5
  │     ├── chunk_002 → data_node_12
  │     └── chunk_003 → data_node_8
  ├── version "3939393940"
  │     ├── chunk_001 → data_node_3
  │     └── chunk_002 → data_node_7
  └── version "3939393941" (archived)
        └── chunk_001 → data_node_2
  • Each PUT creates a new version (if versioning enabled).
  • DELETE adds a delete marker; previous version remains accessible.
  • Old versions can be expired via lifecycle policies.
  • is_latest flag tracks the current version for GET operations.

Data Placement, Durability & Garbage Collection

Data Placement Strategy

Consistent Hashing for Chunk Distribution:

Consistent Hash Ring

         node_A
        /       \
       /         \
  node_D          node_B
       \         /
        \       /
         node_C

Chunk placement: hash(chunk_id) → nearest node clockwise
class ConsistentHash:
    def __init__(self, nodes, virtual_nodes=150):
        self.ring = SortedDict()
        for node in nodes:
            for i in range(virtual_nodes):
                key = hash(f"{node}:{i}")
                self.ring[key] = node
    
    def get_node(self, chunk_id):
        key = hash(chunk_id)
        idx = self.ring.bisect_right(key) % len(self.ring)
        return self.ring.values()[idx]
    
    def get_replicas(self, chunk_id, replication_factor=3):
        nodes = []
        key = hash(chunk_id)
        idx = self.ring.bisect_right(key)
        while len(nodes) < replication_factor:
            node = self.ring.values()[idx % len(self.ring)]
            if node not in nodes:
                nodes.append(node)
            idx += 1
        return nodes

Replication factor = 3: Each chunk stored on 3 different data nodes (ideally across 3 racks or availability zones).

Erasure Coding (Reed-Solomon)

For large objects, erasure coding is more storage-efficient than 3x replication:

Original: [D1] [D2] [D3] [D4]
Encoded:  [D1] [D2] [D3] [D4] [P1] [P2]  (4+2 scheme)

Storage: 6 chunks for 4 data chunks = 1.5x overhead
           (vs 3x for replication)

Fault tolerance: Can survive ANY 2 chunk losses

Reed-Solomon (k, m) scheme:

Scheme Data Chunks Parity Chunks Overhead Fault Tolerance
(3, 1) 3 1 1.33x 1 failure
(4, 2) 4 2 1.50x 2 failures
(6, 3) 6 3 1.50x 3 failures
(10, 4) 10 4 1.40x 4 failures

When to use erasure coding vs replication:

Use Case Strategy Reason
Hot data (< 30 days) 3x replication Fast reads, no decode overhead
Warm data (30–90 days) Erasure coding (4+2) Storage savings, acceptable decode cost
Cold/archive data Erasure coding (6+3) Maximum storage efficiency

Cross-Region Replication

Region A (Primary)              Region B (Replica)
┌──────────────┐               ┌──────────────┐
│  Data Node 1  │──async──────▶│  Data Node 4  │
│  Data Node 2  │──async──────▶│  Data Node 5  │
│  Data Node 3  │──async──────▶│  Data Node 6  │
└──────────────┘               └──────────────┘
       │                              │
       ▼                              ▼
  Metadata Service              Metadata Service
  (Primary)                     (Replica)

Replication flow:

  1. Object PUT in Region A.
  2. Metadata service in Region A records replication status: PENDING.
  3. Background replicator reads new chunks from Region A.
  4. Writes chunks to Region B data nodes.
  5. Updates Region B metadata: replication status COMPLETED.
  6. If replication lag exceeds SLA, alert ops team.

Consistency: Eventual replication with configurable SLA (typically < 15 minutes).

Garbage Collection

Deleted objects and old versions need space reclamation:

Mark-and-Sweep GC:

Phase 1: Mark
  - Identify all chunks referenced by live objects
  - Mark as "live" in chunk reference table
  
Phase 2: Sweep
  - Scan all chunks on data nodes
  - Any chunk not marked as "live" is a candidate for deletion
  - Delete unreferenced chunks after safety window (24 hours)
class GarbageCollector:
    def run_gc(self):
        # Phase 1: Mark live chunks
        live_chunks = set()
        for obj in self.metadata.list_all_objects():
            for chunk in self.metadata.get_chunks(obj.object_id):
                live_chunks.add(chunk.chunk_id)
        
        # Phase 2: Sweep unreferenced chunks
        for node in self.data_nodes:
            for chunk in node.list_chunks():
                if chunk.chunk_id not in live_chunks:
                    if chunk.age > timedelta(hours=24):  # safety window
                        node.delete_chunk(chunk.chunk_id)
                        self.metadata.delete_chunk_record(chunk.chunk_id)

Garbage collection triggers:

  1. Periodic: Run GC every 6 hours.
  2. Threshold: When free disk space drops below 20%.
  3. Event-driven: After bulk delete operations.

Integrity Verification

Mechanism Frequency Scope
Checksum on write Every PUT Data integrity at write time
Checksum on read Every GET Detect silent corruption
Background scan Weekly Full data integrity audit
Replica comparison Daily Detect replica divergence
def verify_chunk_integrity(chunk):
    stored_checksum = chunk.checksum
    calculated_checksum = sha256(chunk.data)
    if stored_checksum != calculated_checksum:
        # Log corruption event
        log.warning(f"Chunk {chunk.chunk_id} corrupted")
        # Repair from healthy replica
        repair_from_replica(chunk)
        # Alert ops
        alert(f"Data corruption detected: {chunk.chunk_id}")

Failure Handling & Self-Healing

┌─────────────────────────────────────────────────┐
│            Failure Detection & Recovery          │
│                                                  │
│  1. Heartbeat monitoring (every 5s)              │
│  2. Node misses 3 heartbeats → marked suspect    │
│  3. After 60s → marked failed                    │
│  4. GC triggers replica rebalancing              │
│  5. New replicas created to maintain RF=3         │
│  6. If node recovers → sync and rejoin           │
└─────────────────────────────────────────────────┘

Disk failure handling:

  1. SMART monitoring detects failing disk.
  2. Data node marks disk as read-only.
  3. Background job copies chunks from failing disk to healthy disks.
  4. Once copy complete, disk is replaced.

Node failure handling:

  1. Cluster manager detects node down.
  2. Rebalance chunks: ensure each chunk has 3 healthy replicas.
  3. New replicas created on remaining healthy nodes.
  4. No data loss as long as <= 2 nodes fail simultaneously.

Performance Benchmarks

Metric Target Actual
PUT latency (1MB) < 200ms 150ms
GET latency (1MB) < 100ms 80ms
GET first byte (100MB) < 500ms 350ms
Throughput per connection 1 Gbps 800 Mbps
Metadata service throughput 100K ops/sec 120K ops/sec
Durability 99.999999999% Verified via audit
Storage efficiency < 1.5x overhead 1.3x (EC 4+2)

Interview Tips

  1. Start with requirements: Clarify durability target (11 nines is standard), object size range, access patterns.
  2. Draw the architecture: API → Metadata → Data Nodes → Storage.
  3. Explain the upload flow: Multi-part upload, chunk distribution, metadata recording.
  4. Discuss durability: Replication vs erasure coding trade-offs, cross-region replication.
  5. Cover consistency: Strong consistency for reads/writes, eventual for LIST and replication.
  6. Address lifecycle: Hot → warm → cold transitions, expiration, MFA delete.
  7. Garbage collection: Mark-and-sweep, safety window, triggers.
  8. Failure modes: Disk failure, node failure, rack failure, region failure — always recoverable.

Practice Problems

0/1solved
Design a Distributed File Storage Service

Design a distributed file storage system similar to Amazon S3 or Google Cloud Storage. The system must support storing billions of objects with 99.999999999% durability, versioning, access control, lifecycle policies, and multi-part uploads for large files.

Quiz

1. What is the difference between erasure coding and replication for achieving data durability?

Question 1 options

2. Why does a file storage system separate metadata from data nodes?

Question 2 options

3. What is the purpose of consistent hashing in the data placement strategy?

Question 3 options

4. In the garbage collection process for a file storage system, why is a safety window (e.g., 24 hours) used before physically deleting unreferenced chunks?

Question 4 options

5. What storage tier would you recommend for data that is accessed once per quarter and needs to be retained for 5 years?

Question 5 options

Flashcards

Question

What is the durability target for a production file storage system like S3?

Answer

99.999999999% (11 nines). This means statistically, you would lose 1 out of 10 trillion objects per year. Achieved via replication + erasure coding + cross-region replication.

Question

What is Reed-Solomon erasure coding and why is it used in file storage?

Answer

Reed-Solomon splits data into k chunks and generates m parity chunks. Any k chunks can reconstruct the original data, tolerating up to m failures. Uses less storage than replication (1.5x vs 3x) while maintaining high durability.

Question

How does multi-part upload work and why is it necessary?

Answer

Large files are split into chunks (8–64MB). Each chunk uploaded independently (parallel/resumable). InitiateMultipartUpload → UploadPart (×N) → CompleteMultipartUpload. Necessary for resumability, parallelism, and avoiding timeout on large files.

Question

Why separate metadata service from data nodes in a file storage architecture?

Answer

Metadata (object index, chunk locations, ACLs) is small but frequently accessed — needs fast storage (MySQL/etcd). Data chunks are large but accessed less frequently — stored on commodity disks. Separation allows independent scaling and optimization.

Question

What is consistent hashing and why is it used for chunk distribution?

Answer

Maps chunks and nodes to a hash ring. Chunks are assigned to the nearest clockwise node. When nodes are added/removed, only nearby chunks rebalance — no full reshuffling. Minimizes data movement during scaling.

Question

How does garbage collection work in a distributed file storage system?

Answer

Mark-and-sweep: (1) Mark all chunks referenced by live objects, (2) Sweep unreferenced chunks on data nodes, (3) Delete after safety window (24h) to handle race conditions. Triggers: periodic (every 6h), threshold (low disk), or event-driven (bulk delete).

Question

What are the storage tiers in a file storage system and when to use each?

Answer

Hot (SSD, <10ms, $0.023/GB): frequent access. Warm (HDD, <50ms, $0.0125/GB): infrequent. Cold (Glacier, minutes, $0.004/GB): archive. Deep Archive (hours, $0.001/GB): long-term retention. Lifecycle policies auto-transition between tiers.

Question

How does a file storage system handle strong consistency for reads and writes?

Answer

Metadata service uses consensus (Raft/Paxos) with synchronous replication to majority. PUT: append to WAL, replicate to majority, commit. GET: read from leader or caught-up follower. Ensures read-after-write consistency for all operations.

Revision Notes

Key Takeaways

  • 1.Separate metadata from data for independent scaling and optimization
  • 2.Erasure coding (4+2 or 6+3) provides high durability with lower storage overhead than 3x replication
  • 3.Consistent hashing enables even distribution with minimal data movement during scaling
  • 4.Multi-part upload provides resumability and parallelism for large objects
  • 5.Mark-and-sweep GC with safety window handles non-atomic metadata/data deletion
  • 6.Strong consistency for reads/writes via consensus (Raft/Paxos) in metadata service
  • 7.Lifecycle policies enable automatic tier transitions (hot → warm → cold → archive)
  • 8.Cross-region replication provides disaster recovery with eventual consistency

Interview Tips

  • Start by clarifying durability target (11 nines) and object size range (1B – 5TB)
  • Draw the architecture early: Client → API → Metadata Service → Data Nodes → Storage
  • Explain multi-part upload flow: Initiate → Upload Parts (parallel) → Complete
  • Discuss durability strategies: replication vs erasure coding, when to use each
  • Cover consistency: strong for reads/writes (metadata uses consensus), eventual for replication
  • Address lifecycle policies: hot → warm → cold transitions, expiration rules
  • Explain garbage collection: mark-and-sweep, safety window, triggers
  • Discuss failure modes: disk failure, node failure, rack failure, region failure
  • Be ready for follow-ups: 'How do you handle 100K concurrent uploads?' → chunk parallelism + consistent hashing
  • Mention cost optimization: storage tiers + lifecycle policies reduce storage costs by 60–80%

Cheat Sheet

File Storage Service (S3/GCS) - Quick Reference

Durability Target: 99.999999999% (11 nines)

Architecture

Client → API Gateway → Metadata Service (MySQL/etcd) → Data Nodes → Storage Disks

Key Design Decisions

  • Metadata/Data separation: Independent scaling
  • Consistent hashing: Even chunk distribution, minimal rebalancing
  • Erasure coding (4+2): 1.5x overhead, tolerates 2 failures
  • Replication (RF=3): For hot data, fast reads

Upload Flow

  1. InitiateMultipartUpload → get upload_id
  2. UploadPart × N (parallel, resumable)
  3. CompleteMultipartUpload → create object metadata

Chunk Sizes

  • < 100MB: single PUT
  • 100MB–1GB: 8MB chunks
  • 1GB–10GB: 16MB chunks
  • 10GB–100GB: 32MB chunks
  • 100GB: 64MB chunks (max 10K parts)

Storage Tiers

Tier Media Latency Cost/GB/mo
Hot SSD <10ms $0.023
Warm HDD <50ms $0.0125
Cold Tape min-hrs $0.004
Deep Archive Tape hrs $0.001

Consistency Model

  • Strong: PUT, GET, DELETE (metadata uses Raft consensus)
  • Eventual: cross-region replication (<15min SLA)

Garbage Collection

  • Mark-and-sweep with 24h safety window
  • Triggers: periodic (6h), threshold (low disk), event-driven

Failure Handling

  • Disk failure: copy to healthy disk, replace
  • Node failure: rebalance chunks, create new replicas
  • Region failure: failover to replica region

Integrity

  • Checksum on write (SHA-256)
  • Checksum on read (detect corruption)
  • Background scan (weekly full audit)
  • Replica comparison (daily)