Forward-deployed / Learning zone
System Designa standalone module
Lesson 05

Storage & sync

TL;DR

Cloud storage systems solve two distinct problems. File sync (Google Drive) keeps user files consistent across devices. It splits them into content-addressed blocks, syncs only deltas, and resolves conflicts. Object storage (S3) stores immutable blobs at planetary scale, with extreme durability guarantees through replication or erasure coding. Both split metadata from data, but for different reasons: file sync needs fast tree traversal and conflict detection, while object storage needs petabyte-scale durability at minimal overhead. Understanding the boundary between these two models determines whether your product stores files that users edit or objects that services consume.

🎯 For the technical PM

Why it matters — Every product stores something. Choosing between file-sync semantics and object-store semantics shapes your upload UX, your consistency model, and your infrastructure cost. Pick wrong and you build conflict resolution for data that never conflicts, or skip it for data that does.

What it changes in your decisions — You evaluate whether your product needs mutable, version-tracked files (documents, collaborative assets) or immutable write-once blobs (media, logs, backups). You size storage tiers — hot vs. cold — and budget for durability overhead (50% for erasure coding vs. 200% for replication).

Ask your eng team — "Are we storing mutable files or immutable objects, and what durability guarantee do we actually need — six nines or eleven?"

Risk if ignored — You promise "Google Drive-like sync" but deliver a dumb upload/download API, or you replicate 3x when erasure coding would halve your storage bill at higher durability.


Google Drive: cloud file storage

Scale and requirements

The block server: splitting files into blocks

The core insight: don't upload entire files — split them into blocks (typically 4 MB), then hash, compress, and encrypt each block independently.

Google Drive - block-level sync architecture
Upload File upload pipeline
Clientfile watcher
→
Block Serversplit into 4MB blocks
→
Hash + CompressSHA-256, gzip
→
EncryptAES-256
→
Cloud StorageS3 / GCS
↘
Metadata DBfile_version, block_list, user_id
Delta sync Only modified blocks are uploaded
Original:
Block 0
Block 1
Block 2
Block 3
16 MB file
▼ user edits middle section
Modified:
Block 0
Block 1*
Block 2
Block 3
Upload 4 MB only
Notification service
File changed → long polling to all synced clients
Client receives event → fetches latest metadata
Downloads only changed blocks
Conflict resolution
Two users edit same file simultaneously:
First commit → wins (version n+1)
Second commit → conflict copy saved separately
User merges manually

Why blocks matter:

Metadata vs. data: the separation principle

The system separates metadata (file names, versions, block lists, sharing permissions, user info) from data (the actual block bytes). They live in different stores with different consistency requirements:

Concern Metadata store Data store
Storage Relational DB (MySQL/PostgreSQL) Cloud/object storage (S3)
Consistency Strong (ACID transactions) Eventual
Scale strategy Sharding by user_id Content-addressed, replicated
Access pattern Small reads, frequent updates Large reads/writes, append-mostly
Failure impact Users can't see their files Users can't open their files

The metadata database schema centers on a file_versions table that maps each file version to an ordered list of block hashes, plus a blocks table that maps hashes to storage locations.

Upload flow

Upload flow

Pre-signed URLs keep block data off the API servers

Two-phase commit: pending until all blocks land, then uploaded.

Client
API servers
Block servers
Cloud storage
Metadata DB
1Client → API: upload request (file metadata)
2API → Metadata DB: create file entry, status=pending
3API → Client: pre-signed upload URLs
4Client → Block servers: upload modified blocks
5Block servers: hash, compress, encrypt → Cloud storage
6Block servers → Metadata DB: update block refs, status=uploaded
7Metadata DB → Notification service → other devices

Key design decisions in the upload path:

Download and sync flow

When a client comes online (or receives a push notification), it:

  1. Asks the metadata service: "What's changed since my last sync checkpoint?"
  2. Receives a list of file changes (new, modified, deleted) with their block lists.
  3. Compares each file's block list against its local cache.
  4. Downloads only the blocks it doesn't already have.
  5. Reassembles files locally: decrypt, decompress, concatenate blocks in order.

Conflict resolution

When two users (or two devices) edit the same file concurrently:

This is deliberately simple. More sophisticated strategies (operational transforms for Google Docs, CRDTs for collaborative editing) are layered on top for specific file types but not built into the storage layer.

Notification service: long polling

Devices need to know when files change. The notification service uses long polling rather than WebSockets:

Why long polling over WebSockets here:

Storage tiering

Not all data is equally hot:

Storage tiering

Cold storage is 5–10x cheaper per GB than hot

Not all data is equally hot — auto-tier by inactivity.

Hot — SSD

Recently modified files

→ 30d inactive →
Warm — HDD

Accessed in last 30 days

→ 90d inactive →
Cold — archival

Untouched 90+ days

↩ User accesses a cold file → promoted back to hot

Cold storage is 5-10x cheaper per GB than hot storage. For a system with 50M users and 10 GB each, the difference between storing everything hot (500 PB at hot prices) vs. tiering (with 80% of data cold) is enormous.


S3-like object storage

Scale and requirements

Core data model: buckets and objects

An object store is flat — no directories, no hierarchy. The "path" is just a key string:

s3://my-bucket/photos/2024/vacation/IMG_0042.jpg
        ^           ^
      bucket      object key (opaque string)

Each object consists of:

Architecture: metadata store + data store

Control plane + data plane

Metadata points to data — it never stores the bytes

A sharded relational DB for pointers; a distributed cluster for the actual bytes.

Control plane
API service
Identity & access management
Metadata store — sharded relational DB
Data plane
Data store — distributed storage nodes
Replication / erasure coding

Metadata store — a sharded relational database (sharded by bucket + object key hash). Stores:

Data store — a distributed cluster of storage nodes that hold the actual bytes. Each node manages local disks and reports health/capacity to a placement service.

The small-file problem and WAL-style merging

Object storage is optimized for large objects, but real workloads include many small files (1-100 KB). Writing each small object as a separate file on the data node wastes:

The solution: WAL-style file merging (write-ahead log). Small objects are appended sequentially to a large "WAL file" on each data node:

WAL-style file merging

Random writes become sequential appends

Small objects share one large WAL file instead of one file each.

obj-a · 2 KB
obj-b · 500 B
obj-c · 8 KB
↓ appended sequentially ↓
offset 0: obj-a (2 KB)
offset 2048: obj-b (512 B)
offset 2560: obj-c (8 KB)

The metadata store records (WAL_file_id, offset, length) for each object. Reads seek to the exact offset in the WAL file. This converts random writes to sequential appends — the fastest I/O pattern on both HDD and SSD.

Durability: replication vs. erasure coding

The two approaches to surviving disk and node failures:

3x Replication:

Erasure coding (8+4):

Property 3x Replication Erasure coding (8+4)
Storage overhead 200% 50%
Durability ~6 nines ~11 nines
Read latency Low (single copy read) Higher (multi-shard read)
Recovery speed Fast (copy) Slow (reconstruct)
Write amplification 3x 1.5x
Use case Hot/active data Warm/cold data

At 100 PB scale, the storage cost difference is staggering: replication needs 300 PB of raw storage, erasure coding needs 150 PB. That difference is millions of dollars annually.

Multipart upload

Large objects (>100 MB) are uploaded in parts:

  1. Client initiates a multipart upload, receives an upload_id.
  2. Client uploads parts in parallel (each part 5 MB to 5 GB), receiving an ETag per part.
  3. Client sends a "complete" request listing all part ETags.
  4. Server assembles the parts into a single object atomically.

Benefits:

If the client never sends "complete" (crashed, abandoned), a lifecycle policy garbage-collects the orphaned parts after a configurable timeout.

Garbage collection and compaction

Objects are never truly deleted in place (the data is immutable). Instead:

  1. A delete request marks the object as deleted in the metadata store (tombstone).
  2. Versioned buckets keep the old version accessible; non-versioned buckets hide it.
  3. A background compaction process periodically scans WAL files, identifies space occupied by deleted/overwritten objects, and rewrites live objects into new WAL files.
  4. The old WAL file is reclaimed only after all references to it are updated in the metadata store.

This is the same compaction model used by LSM-tree databases (LevelDB, RocksDB) — a pattern that appears repeatedly in systems that favor write throughput.

Object versioning and lifecycle

Versioning stores every version of every object:

Lifecycle policies automate storage management:


Failure modes

Practitioner checklist