Storage & Data
Design Google Drive
01
What we are building
What we are building
- Upload, download, rename, move and delete files and folders from web, desktop and mobile clients.
- A desktop agent mirrors a chosen folder and syncs changes in both directions, including edits made while offline.
- Share a file or folder with people or a link, as viewer, commenter or editor.
- Keep version history for 30 days on free plans and a year on business plans, with one-click restore.
- Deleted items sit in a trash folder for 30 days before they are purged.
- Per-account storage quota, enforced at upload time.
- Search file names across everything a user can see.
- Once an upload is confirmed, its bytes survive anything short of a regional catastrophe: target eleven nines of annual durability.
- A change on one device reaches the user's other online devices in under 10 seconds at p95.
- Uploads and downloads resume after a dropped connection without starting over.
- Editing a few bytes in a large file transfers roughly that much data, not the whole file.
- Metadata reads such as folder listings stay under 150 ms at p99.
- Two devices editing the same file offline must never silently lose either version.
Ask whether you are designing file sync, like a desktop agent mirroring a folder, or collaborative editing, like several people typing into one document at once. They are different systems. Sync treats files as opaque bytes and resolves conflicts per file. Live co-editing needs operational transforms or CRDTs inside the document. Most interviewers want sync, with sharing and versions, and are happy for docs editing to be a separate service that stores its output as files.
02
Sizing it
Sizing it
Assume 900 million accounts, 180 million of them active on a given day, with an average of 4 GB stored per account. Each active user produces about three file changes a day across their devices, and the average change carries 1.5 MB of new data after chunking.
| Quantity | Working | Result |
|---|---|---|
| Logical data stored | 900M x 4 GB | 3.6 EB |
| After deduplication | about 25% of chunks are shared copies | about 2.7 EB unique |
| Raw disk with erasure coding | 2.7 EB x 1.5 overhead for a 10+5 code | about 4 EB |
| File changes per day | 180M x 3 | 540M |
| Commit rate | 540M / 86,400 s, peak 3x on weekday mornings | 6,250 / s, peak about 19,000 / s |
| New bytes ingested per day | 540M x 1.5 MB | about 810 TB |
| Ingest bandwidth | 810 TB / 86,400 s, peak 3x | about 9.4 GB / s, peak 28 GB / s |
| Downloads per day | each change fetched by about 1.8 other devices | about 970M, 1.4 PB egress |
| Connected sync clients | 180M users x 1.4 devices online | about 250M open connections |
| File and folder records | 900M x about 2,500 entries | about 2.25 trillion rows |
| Metadata size | 2.25T x 500 B incl. chunk lists and versions | about 1.1 PB |
Unlike most consumer systems, this one is close to balanced between writes and reads, roughly two downloads per upload, because sync moves every change to every other device. The byte volume is enormous but simple: blobs are immutable and addressed by hash. The hard engineering is in 2 trillion metadata rows that must stay transactionally consistent, and in a quarter of a billion idle connections waiting to be told something changed.
03
API design
API design
The upload is split into prepare, transfer and commit for three reasons. Prepare lets the server skip any chunk it already has, which is where dedup and delta sync pay off. Transfer goes directly to block storage on signed URLs, so file bytes never pass through the metadata tier. Commit is a small compare-and-set on the revision number, which is how conflicts are detected without locks.
04
System overview
System overview
flowchart LR
DC["Desktop or mobile client"] --> GW["API gateway"]
GW --> MS["Metadata service"]
MS --> MDB[("Metadata DB, sharded by namespace")]
MS --> J[("Change journal")]
DC -->|"chunk PUT, signed URL"| BS["Block service"]
BS --> OBJ[("Erasure-coded blob store")]
MS -->|"has chunk?"| CI[("Chunk index")]
J -. "new seq" .-> NS["Notification service"]
NS -->|WebSocket| DC
MS -. "events" .-> Q[["Event bus"]]
Q --> SR["Search indexer"]
Q --> GC["Garbage collector"]
DC -->|download| CDN["CDN and block read path"]The system separates bytes from meaning. The block service stores chunks by their hash and knows nothing about files, owners or folders. The metadata service knows everything else: the folder tree, which ordered list of chunk hashes makes up each revision, who can see what, and how much quota is used. Because chunks are immutable, the blob tier can be replicated, cached and moved freely. All of the consistency work is concentrated in the metadata database, which is far smaller.
Every user's root folder and every shared folder is a namespace. A namespace owns its own change journal, an append-only log with a monotonically increasing sequence number. Committing a revision writes the new metadata rows and the journal entry in one transaction on the namespace's shard. A device keeps one cursor per namespace it has mounted, so catching up after a week offline is a single paged read of the journal.
Watches the local folder, splits changed files into chunks, hashes them and keeps a local database of what it last synced. It is half of the system, and most real sync bugs live here.
Stateless API over the sharded metadata store. Handles prepare and commit, folder operations, sharing and quota. Every write is scoped to one namespace, so it stays on one shard.
Accepts chunk uploads on signed URLs, verifies that the content hashes to its name, and writes it into erasure-coded storage. Reads are served through a CDN for shared links.
A large key-value table from chunk hash to storage location and reference count. Prepare uses it to answer "do we have this already", which is the dedup check.
Holds about 250M mostly idle WebSockets across a fleet of connection servers. It only sends "namespace X is now at sequence N", and clients pull the details.
Consume metadata events to update the search index, generate thumbnails and previews, expire old versions and trash, and reclaim chunks no revision points to.
05
Deep dive: chunks, dedup and the sync loop
Deep dive: chunks, dedup and the sync loop
If a file is one blob, a one-byte change means re-uploading the file, and two identical files cost double. If a file is an ordered list of content-addressed chunks, an edit only uploads the chunks that changed, identical chunks anywhere are stored once, and an interrupted upload resumes at the next missing chunk. Every benefit in this design comes from that one representation.
Fixed-size versus content-defined chunks
The obvious approach cuts files every 4 MB. It works well for files that are edited in place, such as virtual machine disks and databases. It fails for the common case of inserting data near the start of a file: every boundary after the insert shifts, every chunk hash changes, and a small edit re-uploads the whole thing.
Content-defined chunking places boundaries where a rolling hash over a small sliding window matches a pattern, for example when its low 21 bits are zero, which gives an average chunk around 2 MB. Because boundaries depend on content and not on offset, an insertion only changes the one or two chunks around it, and the rest of the file produces the same hashes as before. We enforce a minimum of 512 KB and a maximum of 8 MB so that pathological files do not create millions of tiny chunks or one giant one. Files under 512 KB are a single chunk.
flowchart TD
E["User saves file on laptop"] --> CH["Split with rolling hash, SHA-256 each chunk"]
CH --> PR["prepare: send chunk list and base revision"]
PR --> MI{"Server: any chunks missing?"}
MI -->|yes| UP["PUT missing chunks to block store"]
MI -->|no| CM["commit with base revision"]
UP --> CM
CM --> OK{"Base revision still current?"}
OK -->|yes| JR["Write revision and journal entry"]
OK -->|no| CF["Keep both: create conflicted copy"]
JR --> NT["Notify devices: namespace at seq N"]
NT --> PH["Phone pulls changes since its cursor"]
PH --> DL["Phone downloads only chunks it lacks"]Deduplication and its limits
Because a chunk's name is the SHA-256 of its bytes, the chunk index can tell the client that a chunk already exists, and the upload is skipped. Within one account that is uncontroversial: it catches copies, moved files and reverted edits. Across accounts it saves more space, but it leaks information. If an attacker can learn that a chunk was skipped, they can confirm whether any other user has a specific file. The usual compromise is to deduplicate globally on the server but always ask the client to upload chunks it has not personally stored before, and then discard the duplicate bytes. You keep the storage savings and give up the bandwidth savings only for cross-account matches.
Deduplication also changes deletion. A chunk can be referenced by many revisions across many users, so it can only be removed when nothing points to it. Reference counts updated inside every commit are fast but fragile: one lost decrement leaks space forever, one extra decrement deletes live data. We use counts as a hint and run a periodic mark and sweep over the metadata to confirm before any chunk is physically deleted, with a grace period of several days.
Keeping devices in step
Each device tracks the last journal sequence it applied per namespace. When the notification channel says a namespace has moved ahead, the device calls the changes endpoint with its cursor and applies entries in order. If the WebSocket drops, nothing is lost: the client reconnects, compares cursors and catches up. If a device has been offline so long that the journal has been compacted past its cursor, the server returns a reset signal and the client does a full listing of that namespace instead.
Conflicts are detected at commit time. A commit names the revision it was based on. If that is no longer the latest revision, the commit is rejected and the client keeps both versions, saving its own as report (Lena's laptop, conflicted copy 2026-10-01).docx. That is less elegant than merging, but the service has no idea how to merge arbitrary binary files, and losing either version is the one outcome the requirements forbid.
06
Design decisions
Design decisions
Neither edit is lost and the user decides. The cost is clutter: a busy shared folder can collect conflicted copies, which the product should surface clearly.
Safe for any file typeNo clutter, but device clocks drift and the losing edit simply vanishes. Version history can recover it only if someone notices, which in practice they rarely do until it matters.
Silent data lossThe push channel carries only a sequence number, so it can be lossy, deduplicated and cheap to fan out. Correctness lives in the cursor and the journal. Updates arrive within seconds and idle devices cost a few kilobytes of memory each.
Fast and self-healing250M devices polling every 30 seconds is over 8 million requests a second, nearly all answering "nothing new". Making the interval longer makes sync feel broken.
Fallback onlyEach tier scales on its own axis. Metadata needs transactions across a revision, its journal entry and quota; a relational database sharded by namespace gives that without cross-shard commits. Blobs need cheap, durable capacity and nothing else.
Right tool per tierExabytes of blobs would make backups, replication and failover impossible, and every large read would compete with metadata queries for the same disks and caches.
Does not scaleMoving a folder with 80,000 files changes one row. Listing a folder is an index lookup on parent id. Resolving a full path walks up the tree, which we cache, and the journal records paths at commit time so clients do not have to.
Cheap moves, cheap listingsMake "everything under this folder" a single range or join, which helps permission checks and subtree search. But a move rewrites a row for every descendant, which is painful when users drag a large folder somewhere new.
Good for read-mostly treesA 10+5 code survives five lost fragments at 1.5x overhead, against 3x for triple replication. New chunks start replicated for fast reads and are re-encoded after a few days, once access drops.
Halves raw diskSimpler reads and repairs, and the right choice at small scale. At 2.7 EB of unique data, 3x instead of 1.5x costs about 4 EB of extra disk.
Too costly at this size07
Things that go wrong
Things that go wrong
Someone uploads a 6 GB video from a train. The network drops at 70%. Without care, the next attempt starts from zero, and on a bad connection it never finishes.
Chunks are independent, so the client simply calls prepare again: the server reports which chunks are still missing and the upload continues from there. Uploaded chunks without a commit are kept for 24 hours under the upload id, then collected.A commit lands, but one referenced chunk was never fully written, or a bug in garbage collection removed it. The file looks fine in listings and fails to open.
Commit checks the chunk index for every hash before it writes the revision, and the block service only marks a chunk present after the bytes verify against the hash. Garbage collection runs mark and sweep with a multi-day grace window, and a background scrubber reads random chunks to catch silent corruption.An account has 300 MB left. Two devices each commit a 250 MB file at the same moment, both read the same remaining quota, and both succeed.
Keep the quota counter on the same shard as the account's root namespace and update it in the commit transaction with a conditional decrement. Shared folders charge the owner's quota through a reservation on the owner's shard, taken before the commit.A connection server fleet restarts, or an ISP outage ends. Tens of millions of clients reconnect and immediately ask for changes, and the metadata tier sees a spike far beyond normal peak.
Clients reconnect with randomised exponential backoff. The notification service sends each reconnecting client its namespaces' latest sequence numbers, so clients already up to date make no further calls. The changes endpoint is rate-limited per account with a clear retry-after.A company shares one folder with 40,000 employees and millions of files. Every commit in it notifies 40,000 people's devices, and its shard handles far more traffic than any other.
Move very large namespaces onto dedicated shards, coalesce notifications so a burst of commits sends one signal per device per second, and paginate change reads aggressively. For the largest teams, split one shared drive into several namespaces.A faulty release of the desktop agent misreads its local database and decides that thousands of files were deleted locally. It dutifully syncs the deletions to every device.
Deletes go to trash, never straight to purge. The server watches for unusual mass deletes from one device and pauses them for user confirmation, and restore-from-point-in-time rolls a namespace back to any journal sequence.08
Common mistakes
Common mistakes
Spreadsheets and design files get saved dozens of times an hour. Re-sending 80 MB for a one-cell change wastes the user's bandwidth and the provider's ingest capacity.
The push channel becomes a second, unreliable transfer path. Missed messages mean missed data, and large payloads tie up connection servers built to hold idle sockets.
The same chunk may belong to older versions, to a copy in another folder or to another account entirely. Immediate deletion corrupts files you never touched.
09
Interview advice
Interview advice
Split bytes from metadata on your first diagram. Say that blobs are immutable and addressed by hash, and that all the hard consistency questions live in metadata. It organises the rest of the conversation.
Explain chunking with a concrete edit. Walk through inserting a paragraph at the top of a document, show why fixed blocks shift, and introduce content-defined boundaries as the fix.
Describe the upload as prepare, transfer, commit. It covers resumable uploads, dedup and conflict detection in one flow, and shows that file bytes skip your API servers.
Use a journal and cursors for sync. Interviewers often push on "what if the device was offline for a month". A per-namespace sequence number makes the answer simple and makes missed notifications harmless.
Take a clear position on conflicts. Say you keep both versions and why, and mention that real-time co-editing is a different system built on operational transforms or CRDTs.
Raise the dedup privacy issue yourself. Cross-account dedup can reveal that someone else holds a file. Knowing that, and the server-side-only workaround, is a strong signal.
Talk about deletion as carefully as upload. Trash, version expiry and chunk garbage collection are where real systems lose data. Bring up mark and sweep with a grace period.
Quantify durability. Explain erasure coding, what a 10+5 layout survives, and why its overhead matters at exabyte scale. Numbers here make the answer credible.
10
Growing the system
Growing the system
Upload the full file to object storage, write a row per file with a version number, and have clients poll every minute. It is enough to validate the product, and the bandwidth bill is still small.
Large files and frequent saves make full uploads painful. Introduce chunking, per-account dedup and the prepare and commit flow. Replace polling with a notify channel plus cursors.
The single metadata database runs out of write capacity. Model every root and shared folder as a namespace, shard by namespace id and give each its own journal so commits stay single-shard.
Storage cost now dominates. Move to an in-house erasure-coded blob store, dedup across accounts on the server side, and run continuous scrubbing and mark and sweep collection.
Enterprises want data kept in a chosen region and folders shared by tens of thousands. Pin namespaces to regions, give huge namespaces dedicated shards and coalesce their notifications.
Watch and read
Further watching and reading
Try next
Free to read · better with landed
Whiteboard this with landed
landed runs it as a live system design round on the whiteboard and grades your trade-offs.

