← System design

Storage & Data

Design Google Drive

A cloud drive has two jobs that pull in opposite directions. Storage wants files broken into immutable, deduplicated chunks that never change once written, while sync wants every edit on one laptop to show up on a phone and a coworker's desktop within seconds. The design lives in the metadata layer that ties those chunks into files, folders, versions and permissions, and in how devices find out what changed without asking constantly.
Core: content-addressed chunks + change journal900M accounts, 3.6 EB storedWrite-heavy sync, bursty readsNever lose a committed byte

01

What we are building

02

What we are building

Functional
  • 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.
Non-Functional
  • 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.
Narrow the scope early

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

03

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.

QuantityWorkingResult
Logical data stored900M x 4 GB3.6 EB
After deduplicationabout 25% of chunks are shared copiesabout 2.7 EB unique
Raw disk with erasure coding2.7 EB x 1.5 overhead for a 10+5 codeabout 4 EB
File changes per day180M x 3540M
Commit rate540M / 86,400 s, peak 3x on weekday mornings6,250 / s, peak about 19,000 / s
New bytes ingested per day540M x 1.5 MBabout 810 TB
Ingest bandwidth810 TB / 86,400 s, peak 3xabout 9.4 GB / s, peak 28 GB / s
Downloads per dayeach change fetched by about 1.8 other devicesabout 970M, 1.4 PB egress
Connected sync clients180M users x 1.4 devices onlineabout 250M open connections
File and folder records900M x about 2,500 entriesabout 2.25 trillion rows
Metadata size2.25T x 500 B incl. chunk lists and versionsabout 1.1 PB
Reading the table

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

05

API design

POST/v3/files/{file_id}/revisions:prepareAnnounce a new version and learn which chunks are missing
// Client has already split the file and hashed each chunk with SHA-256 { "base_revision": 41, "size": 73400320, "chunks": ["9f2c...e1", "04ab...7d", "c31e...90"] } // 200: only one chunk is new; the rest already exist somewhere { "upload_id": "up_6TQ1k", "missing": [ { "hash": "04ab...7d", "put_url": "https://blk-eu3.example.net/c/04ab...?sig=..." } ], "expires_in": 3600 }
POST/v3/files/{file_id}/revisions:commitMake the new version visible
{ "upload_id": "up_6TQ1k", "base_revision": 41 } // 200 { "revision": 42, "journal_seq": 88213077 } // 409 if revision 41 is no longer current: someone else committed first // 413 if the commit would push the account over quota
GET/v3/changes?namespace=ns_7781&since=88212900&limit=500Pull everything that changed after a cursor
{ "entries": [ { "seq": 88213077, "op": "upsert", "path": "/Taxes/2026/receipts.xlsx", "revision": 42 }, { "seq": 88213102, "op": "delete", "path": "/Taxes/draft.txt" } ], "cursor": 88213102, "has_more": false }
GET/v3/notify (WebSocket)Lightweight signal that changes exist
// Server to client, carries no file data at all { "namespace": "ns_7781", "latest_seq": 88213102 } // Client compares with its cursor and calls /v3/changes if behind
POST/v3/sharesGrant access to a folder
{ "item_id": "fld_220a", "grantee": "lena@example.org", "role": "editor" }

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

06

System overview

Upload, commit and notify pathsDiagram
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.

Client sync engine

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.

Metadata service

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.

Block service

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.

Chunk index

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.

Notification service

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.

Background workers

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

07

Deep dive: chunks, dedup and the sync loop

Why chunking is the whole trick

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.

One edit flowing from laptop to phoneDiagram
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

08

Design decisions

Chosen
Conflicts produce a second file

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 type
Rejected
Newest timestamp wins

No 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 loss
Chosen
Thin push signal, then pull from the journal

The 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-healing
Rejected
Clients poll on a timer

250M 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 only
Chosen
Blob store for bytes, sharded relational store for metadata

Each 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 tier
Rejected
Store file contents inside the database

Exabytes 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 scale
Chosen
Parent id per item, path resolved and cached

Moving 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 listings
Alternative
Materialised full paths or closure tables

Make "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 trees
Chosen
Erasure coding for cold data, replication for hot

A 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 disk
Alternative
Triple replication everywhere

Simpler 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 size

07

Things that go wrong

09

Things that go wrong

📶
The connection drops halfway through a large upload

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.
🧩
Metadata points at a chunk that is not there

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.
⚖️
Two uploads both squeeze under the quota

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.
🌩️
Millions of clients reconnect at once

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.
🏢
One shared namespace becomes enormous

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 client bug deletes files everywhere

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

🚫
Uploading the full file on every save

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.

Instead: chunk by content, hash each chunk and send only the ones the server does not already have.
🚫
Pushing file contents over the notification channel

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.

Instead: push only "something changed", and let the client pull exact changes from the journal by cursor.
🚫
Deleting chunks as soon as a file is removed

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.

Instead: reclaim chunks only after a confirming sweep shows nothing references them, and keep a grace period.

09

Interview advice

10

Interview advice

1

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.

2

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.

3

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.

4

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.

5

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.

6

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.

7

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.

8

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

12

Growing the system

Stage 1: first 50,000 users
Whole files in object storage, one Postgres

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.

Stage 2: a few million users
Chunks, hashes and a change journal

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.

Stage 3: tens of millions
Sharded metadata and namespaces

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.

Stage 4: hundreds of millions
Own block storage and global dedup

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.

Stage 5: global business customers
Regional residency and large shared drives

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

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.