Design Google Drive / Dropbox
Design a file-sync service: split files into content-hashed chunks for dedup and delta sync, separate the metadata DB from the blob store, sync changes to every device with versioning and conflict resolution, and push notifications so clients pull only what changed.
You edit one paragraph in a 200 MB design document. The naive sync client notices the file’s modified-time changed, so it re-uploads all 200 MB — then your laptop, your phone, and your desktop each download all 200 MB back. A one-line edit cost you 800 MB of transfer and a minute of waiting. Now imagine two devices edit the file while one was offline: the client that syncs last silently overwrites the other’s work, and the user loses an afternoon. A file-sync service lives or dies on two questions it must answer well: when something changes, how little data can I move, and when two people change the same thing, how do I avoid eating someone’s work? The answers — chunk the file and move only the chunks that differ, and never blindly overwrite — drive an architecture where the file’s bytes and the file’s story (its metadata and version history) are stored and synced as two completely different things.
Requirements
What turns “store a file” into a hard distributed systems problem? Ask yourself: what happens when two devices edit the same file while offline, and what does “sync” cost in bytes when a user has ten devices? The answers force the whole design. Functional — a user stores files and folders; edits on one device appear on all their other devices and any shared collaborators; files have version history (restore an old version); folders can be shared with specific people at specific permission levels (view/edit). The hard functional requirement hiding in “appear on all devices” is sync: a change must propagate quickly and correctly, even across offline edits.
Non-functional — sync must be fast and bandwidth-efficient (a tiny edit must not move the whole file), reliable (never lose or corrupt a user’s data — durability is sacred), and consistent enough that two devices converge on the same state without silently dropping edits. Reads (downloads, browsing) dominate but writes (edits) are frequent and must be cheap. Storage cost matters because users keep everything and duplicate files everywhere.
The two non-functional facts that shape everything: move the minimum bytes (→ chunking and delta sync) and never lose an edit (→ versioning and conflict resolution, not overwrite).
Estimation
Users: ~500M registered, ~100M daily active
Files: avg user ~50 GB stored; billions of files total
Edits: DAU × a handful of file changes/day → ~1e8 users × ~5 = ~5e8 edits/day
÷ ~1e5 s ≈ ~5,000 edits/sec average, ~15K peak
Chunk: fixed ~4 MB chunks → a 200 MB file ≈ 50 chunks
an edit touches 1–2 chunks, so delta sync moves ~8 MB not 200 MB
Dedup: same file shared/copied by many users stores one physical copy
→ cross-user dedup can cut stored bytes by a large factor for common files
Metadata: per file: id, name, parent, version, chunk list, perms → a few KB
billions of files × few KB → terabytes of metadata (sharded relational/KV)The estimate makes the split obvious: blob bytes are petabytes (object storage, deduplicated), metadata is terabytes (a sharded transactional store). And delta sync turns a 200 MB re-upload into an 8 MB chunk transfer — a 25× bandwidth win on every edit. These numbers are the justification for chunking and the metadata/blob split.
High-level design
A file is modelled as metadata (name, folder, version, an ordered list of chunk hashes, permissions) plus a set of chunks (the actual bytes, content-addressed). The metadata lives in a sharded transactional database; the chunks live in an object/blob store, keyed by their content hash. A sync service plus a notification service keep devices up to date.
The ordering matters: upload chunks first, then commit the new version. If you committed metadata first, a version could reference chunks that aren’t in the store yet, and a reader would get a dangling pointer. Chunks-then-metadata means the metadata always points at bytes that exist — chunks are immutable and content-addressed, so re-uploading the same chunk is a harmless no-op.
Deep dive: chunking, content-addressing, and dedup
The core idea is content-addressed chunks: split each file into chunks and key each chunk by the hash of its contents (e.g. SHA-256). This single idea buys three things at once. Dedup: two identical chunks hash the same, so they are stored once — across versions, across files, even across users (the same shared PDF stored by a thousand people is one physical chunk). Delta sync: to sync a change, the client re-chunks the file, compares chunk hashes to the previous version, and uploads only the chunks whose hashes are new — a one-paragraph edit touches one chunk, so you move ~4 MB instead of 200 MB. Integrity: the hash is the address, so a corrupted chunk is detectable (its bytes no longer match its hash).
Fixed-size chunking (every 4 MB) is simple but brittle: insert one byte at the start of a file and every subsequent chunk boundary shifts, so every chunk hash changes and dedup collapses. Content-defined chunking (rolling-hash boundaries, à la rsync/Rabin) places boundaries based on content, so an insertion only changes the one chunk it lands in — the rest stay aligned and dedup survives. The tradeoff is variable chunk sizes and more CPU; many systems use fixed chunks for simplicity and accept that inserts re-sync more than strictly necessary.
▸Why this works
Why store the bytes and the metadata in two different systems instead of one? Because they have opposite access patterns and requirements. Metadata is small, must be queried and transactionally updated (rename, move, share, version-commit), and benefits from indexes and ACID — a relational/transactional store’s home turf. Chunks are large, immutable, write-once-read-many, never queried by content, and need cheap durable bulk storage with high throughput — an object store’s home turf. Forcing both into one system means either a database choking on petabytes of blobs (slow, expensive, can’t index them anyway) or an object store pretending to be a transactional metadata index (it isn’t). The split lets each scale and be consistent on its own terms, and it’s why “metadata DB + blob store” recurs in every media design in this unit.
Deep dive: conflict resolution and notifications
Two devices edit the same file; one was offline. When both sync, the metadata service sees two new versions branching from the same parent version. The cardinal rule is: never silently overwrite. A last-writer-wins overwrite is the bug from the hook — it eats someone’s work. Instead, detect the divergence (the incoming version’s parent is no longer the current head) and resolve it: for files Drive/Dropbox keep both as conflicted copies (report (Anna's conflicted copy).docx) so a human decides; for structured collaborative documents, operational transforms or CRDTs can merge edits automatically. The version chain (each version records its parent) is what makes conflicts detectable in the first place — without it you can’t tell an overwrite from a legitimate sequential edit.
Propagation uses a notification service, not polling. Each client holds a long-lived connection (long-poll or WebSocket); when a file the client cares about changes, the service pushes a lightweight “something changed” signal, and the client then pulls the metadata diff and any missing chunks. Notification is cheap and fan-out-friendly (just a ping); the actual data transfer is the delta-sync pull the client drives. This keeps the notification path fast and stateless-ish while the heavy lifting stays on the efficient delta path.
▸Common mistake
A subtle, expensive mistake: making clients poll the metadata service (“any changes since version N?”) on a short interval to feel responsive. With 100M devices polling every few seconds, you generate a constant tidal wave of mostly-empty responses — the server spends its capacity answering “nothing changed” billions of times. The fix is to invert it: clients hold a long-poll/WebSocket connection and the server pushes when (and only when) a relevant change happens. Push turns N idle devices into N cheap idle connections instead of N×(1/interval) wasted requests per second. Poll for low device counts, push at scale — and at Drive scale you have no choice but push.
A user edits one paragraph in a 200 MB document. How does an efficient sync service propagate this, and why?
Two devices edit the same file; one was offline. Both now sync to the server. What must the design do?
Each chunk is keyed by the hash of its own bytes, so two identical chunks — across versions, files, or even different users — are stored only once. This single-physical-copy property is called _______.
Bottlenecks & tradeoffs
The dominant resource saved is bandwidth (delta sync) and storage (dedup); both come from content-addressed chunking, which costs client-side CPU to hash and chunk. Fixed vs content-defined chunking trades simplicity for insert-resilient dedup. Conflict policy trades automatic-merge convenience (OT/CRDT, complex, format-specific) against conflicted-copy safety (simple, never wrong, but punts to the human). Push vs poll trades a small amount of connection state for a massive reduction in wasted requests at scale. The deepest tradeoff is consistency of metadata: the metadata commit is the linearization point — it must be transactional (a version commit either fully happens or doesn’t), even though chunk uploads are eventually-consistent best-effort. Get the metadata layer’s consistency right and the blob layer can be loose; reverse them and you corrupt data. As with every design in this unit, the metadata service is where you spend your consistency budget and the blob store is where you spend your bytes.
- 01What does content-addressed chunking buy you, and why split metadata from blobs?
- 02How are conflicting edits detected and resolved without losing data?
- 03Why push change notifications instead of polling, and what's the commit ordering?
A file-sync service is built around two questions: move the fewest bytes on a change, and never lose an edit. The answer to the first is content-addressed chunking — split each file into chunks keyed by their content hash, which simultaneously gives dedup (identical chunks stored once, across versions, files, and users), delta sync (upload only chunks whose hashes are new, turning a 200 MB re-upload into a ~4 MB chunk transfer), and integrity (the hash is the address). That forces the central structural split: a file is metadata (name, folder, version, ordered chunk-hash list, permissions) in a transactional, sharded DB, plus chunks (immutable bytes) in a blob/object store — two systems because their access patterns are opposite. The answer to the second question is the version chain plus a never-overwrite rule: each version records its parent, so a divergent sync (parent no longer the head) is detectable as a conflict and resolved by keeping a conflicted copy (or auto-merging collaborative docs via OT/CRDT) rather than silently clobbering work. Changes propagate by push (long-poll/WebSocket notification → client pulls the diff), never short-interval polling, which would drown the server at device scale. And the commit ordering — chunks first, then version — keeps metadata pointing only at bytes that exist. As across this whole unit: the metadata service holds your consistency, the blob store holds your bytes. Now when you encounter a “files are out of sync” bug report, you know exactly where to look first: is the conflict detection comparing the right parent version, and did the chunk upload complete before the metadata commit?
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.