SYSTEM DESIGN · DESIGN PROBLEMS · MEDIA AND STORAGE · MEDIUM · 15 OF 41

Design Google Drive: Chunked Uploads, Deduplication, Sync and Conflicts

How to design a cloud file storage and sync service like Google Drive or Dropbox: requirements, storage and traffic math, splitting files into content-addressed blocks, resumable uploads and deduplication, separating metadata from blobs, a change journal with cursors so every device syncs, push notifications, conflict handling and version history, sharing permissions, and the trade-offs.

Reported atMicrosoftAppleUberRazorpay

Try it on the practice board Drag the blocks, wire them up, and check your design against the reference.

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
1 / 18

Press play to hear the narration, or step through at your own pace.

Notes

Listen

Requirements. Users upload and download files, up to around ten gigabytes. They organise them in folders: rename, move, delete. Any change syncs automatically to all their devices. They can share files and folders with view or edit permission, and restore older versions for thirty days. Real-time co-editing of documents is a separate problem, see the Google Docs design, and full-text search is out of scope.

Qualities, in priority order. Durability above everything: losing a user's file is the worst possible bug, so aim for the eleven nines that object stores offer. Transfers must be efficient, because people edit big files on slow connections and you should only send what changed. Metadata must be strongly consistent, so a move or rename is all or nothing. Sync should take seconds.

Functional requirements
  • in scope:
  • • upload and download files
  • up to ~10 GB
  • • folders, rename, move, delete
  • • automatic sync to all of a
  • user's devices
  • • share with view or edit
  • • version history (30 days)
  • out: real-time co-editing of
  • documents, full-text search
Non-functional requirements
  • durability: never lose a file
  • (eleven nines target)
  • sync latency: seconds
  • efficient: send only changed
  • parts over slow networks
  • consistent metadata: a folder
  • never shows a half-moved
  • file
  • available, but correctness
  • of metadata comes first

Ask scale

Assume five hundred million registered users, a hundred million active daily, each with about two devices. Each active user writes two files a day: two hundred million writes, about two thousand a second, six thousand at peak. Metadata traffic is much higher: listing folders and checking for changes adds up to around fifty thousand requests a second. And we have well over a hundred million devices waiting to hear about changes.

Storage. At half a megabyte of new data per write, that's a hundred terabytes a day, over thirty petabytes a year. Deduplication claws back a good chunk, because people store the same files, and versions add some back. Object storage with erasure coding keeps durability at around one and a half times the raw size, much cheaper than three full copies. Metadata is about a kilobyte per file: a hundred billion files is a hundred terabytes, which a sharded relational database can hold.

Traffic
registered users     500M
DAU                  100M
devices per user     ~2

file writes/user/day 2
  200M / 100,000 s  = 2K wps
  peak x3           = 6K wps

metadata reads, list + sync
  100M x 50 = 5B/day = 50K rps
open sync connections ~150M
Storage
avg new data/write    0.5 MB
200M x 0.5 MB      = 100 TB/day
  x 365            = ~36 PB/yr

dedupe saves       ~30-50%
versions add       ~20%
object store keeps ~1.5x
  overhead (erasure coding)

metadata 1 KB per file
  x 100B files     = 100 TB

Define API

The API separates bytes from metadata. First the client sends the hashes of a file's blocks and asks which ones the server is missing. It uploads only those, each addressed by its hash. Then it commits a new version: the list of block hashes plus the version it started from. To sync, a device asks for changes since its cursor. Downloads fetch blocks by hash. Sharing is its own call.

Core API
POST /v1/blocks/check
  { hashes[] } -> { missing[] }
PUT  /v1/blocks/{sha256}  (bytes)
POST /v1/files/commit
  { path|fileId, blocks[],
    baseVersion } -> { version }
GET  /v1/changes?cursor=..
  -> { changes[], cursor }
GET  /v1/blocks/{sha256}
POST /v1/files/{id}/share

Data model

Data model. A file row has an owner, a parent folder and a pointer to its latest version. A version row is an ordered list of block hashes. A block row is keyed by its SHA-256 hash with a reference count, so identical content is stored once. A per-user journal records every change with an increasing sequence number. Use a relational database for metadata, because moving a folder or renaming a file must be a single transaction.

Records
file    (fileId, ownerId, parentId,
         name, latestVersion, deleted)
version (fileId, version, size,
         blockHashes[], ts, deviceId)
block   (sha256 PK, size, refCount)
journal (userId, seq, fileId, op)
acl     (fileId, principal, role)

SQL for metadata: moves and
renames need transactions

High-level design

The upload path. The client splits the file into blocks and hashes each one. It asks which of the forty blocks the server lacks; for an edited file, maybe only three. It uploads those three to the block service, which writes them to object storage keyed by hash. Then it commits version eight to the metadata service, which checks every block exists and writes the new version in one transaction.

The sync path. In the same transaction, the commit appends an entry to the user's change journal. The notification service tells the user's other devices that something changed, without saying what. Each device then asks for changes since its last cursor, sees that this file is now version eight, compares block lists, and downloads only the three blocks it doesn't have.

Deep dive: chunking

Deep dive one: chunking. Fixed four-megabyte blocks are simple, but insert a byte near the start of a file and every block boundary shifts, so everything looks new. Content-defined chunking fixes that: a rolling hash slides over the bytes, and you cut a chunk wherever the hash matches a pattern. Boundaries follow the content, so an insert only changes the chunk around it. Each chunk's key is its SHA-256, which makes deduplication automatic.

Content addressing has more benefits. Uploads are naturally resumable: a dropped connection loses one block, and on retry the client asks again which blocks are missing. Re-uploading a block is harmless because the key is its hash. The server recomputes the hash to verify integrity. One caution: deduplicating across users lets an attacker test whether someone else stored a given file, so many designs dedupe only within a user or account.

Blocks and dedupe
  • fixed 4 MB blocks: simple, but
  • insert 1 byte at the start
  • and every block shifts
  • content-defined chunking:
  • cut where a rolling hash
  • matches, ~1-8 MB chunks
  • -> edits change few chunks
  • block key = SHA-256(bytes)
  • same hash = already stored
Resumable and safe
  • each block is its own upload:
  • a dropped connection loses
  • one block, not the file
  • retries are idempotent:
  • same hash, same key
  • server verifies the hash
  • cross-user dedupe leaks
  • "does this file exist?"
  • -> dedupe per user, or add
  • rate limits and encryption

Deep dive: sync

Deep dive two: sync. The backbone is the journal: every commit appends an entry with a sequence number that only grows. Each device remembers the last sequence it applied, its cursor. To sync, it asks for everything after the cursor and moves the cursor forward. Notifications are just a nudge to check now, sent over a long poll or mobile push. If a nudge is lost, a periodic poll catches up, because the journal, not the notification, is the source of truth.

The desktop client does a lot of work. A file watcher notices edits. A small local database maps every path to the last synced version and its block hashes, so the client can tell exactly which blocks changed. It waits a couple of seconds after the last write, so saving a big file doesn't trigger ten uploads. Remote changes are written to a temporary file and swapped in atomically. Offline edits are queued and replayed later.

Change journal
  • every commit appends:
  • (userId, seq, fileId, op)
  • device keeps a cursor = last
  • seq it has applied
  • GET /changes?cursor=1041
  • -> entries 1042.. + new cursor
  • nudge: long poll or push
  • ("check now"), no payload
  • missed nudge? periodic poll
  • catches up anyway
The client side
  • file watcher detects edits
  • local DB: path -> last synced
  • version + block hashes
  • debounce: wait ~2 s after the
  • last write before uploading
  • apply remote changes to a
  • temp file, then atomic rename
  • offline: queue local commits,
  • replay when back online

Deep dive: conflicts

Deep dive three: conflicts. Two devices edit the same file offline, both starting from version seven. Each commit says which version it was based on. The first commit wins and becomes version eight. The second is rejected because its base is no longer the latest. Rather than guess at merging arbitrary files, the losing device keeps its work as a clearly named conflicted copy. Versions are cheap because they share unchanged blocks, and blocks unused by any version are deleted later by a reference-counting job.

Conflicts and versions
  • commit carries baseVersion
  • server: if latest != base
  • -> reject (optimistic lock)
  • losing device saves its copy
  • as "report (conflicted
  • copy, laptop).docx"
  • versions: keep all for 30 days
  • blocks shared, so cheap
  • old blocks -> cold tier
  • refCount 0 -> delete later

Trade-offs

Failures. If blocks upload but the commit fails, those blocks are orphans with no references, and garbage collection removes them later. The metadata database is the source of truth, so shard it by owner, replicate synchronously and fail over fast. If a notification node dies, devices reconnect and read the journal, losing nothing. A folder shared with a whole company is a hot spot: cache its listing. Monitor sync lag, commit error rate and blocks awaiting garbage collection.

Trade-offs to name. Relational metadata gives you transactional moves and renames at the cost of harder sharding; shared folders that cross owners are the awkward case. Conflicted copies are simple and never lose data, but push merging onto the user. Deduplicating per user favours privacy over maximum savings. Erasure coding is far cheaper than three copies but slower to rebuild after a disk loss.

Failures and monitoring
  • block uploaded, commit fails:
  • orphan block, GC by refCount
  • metadata DB is the source of
  • truth: shard by ownerId,
  • sync replicas, fast failover
  • notify node dies: devices
  • reconnect, poll the journal
  • hot shared folder: cache its
  • listing, rate limit writes
  • watch: sync lag, commit errors
Trade-offs to name
  • SQL metadata: consistency for
  • harder sharding
  • conflicted copies: simple and
  • safe, but users must merge
  • per-user dedupe: privacy over
  • maximum savings
  • erasure coding: cheaper than
  • 3 copies, slower to rebuild
  • 10x: shard by owner, tier more

Say it like this

The summary. Files are split into content-defined chunks keyed by their hash, so uploads send only missing blocks, are resumable, and dedupe for free. A file version is an ordered list of block hashes, committed transactionally in a relational metadata store with an optimistic base-version check. Every commit appends to a per-user journal; devices get a nudge and pull changes since their cursor. Conflicts produce a conflicted copy.

The 60-second answer
  • split files: content-defined
  • chunks, key = SHA-256
  • upload only missing blocks
  • commit version = block list,
  • with baseVersion check
  • SQL metadata, object storage
  • for blocks
  • journal + cursor per device
  • nudge by long poll or push
  • conflict -> conflicted copy

Interview questions

Why split files into blocks?

Blocks let you upload only the parts that changed, resume a dropped transfer from the last block, upload blocks in parallel, and deduplicate identical content. A ten-gigabyte file edited in one place costs one block of upload rather than ten gigabytes.

What is content-defined chunking and why use it?

Chunk boundaries are chosen where a rolling hash of the bytes matches a pattern, rather than every fixed number of bytes. An insertion then changes only the nearby chunk instead of shifting every boundary after it.

How do other devices learn about a change?

Each commit appends to a per-user change journal with an increasing sequence number. Devices get a lightweight nudge by long poll or push, then fetch all journal entries after their saved cursor and download the missing blocks.

How do you handle two devices editing the same file offline?

Each commit names the version it was based on; the server accepts the first and rejects the second because its base is stale. The second device saves its edits as a conflicted copy so nothing is lost and the user decides how to merge.

SQL or NoSQL for metadata?

SQL, because renames, moves and commits must be atomic and consistent, and the metadata fits in a sharded relational database. File contents go to object storage, which is built for durable, cheap blobs.

Is deduplication across users a good idea?

It saves the most storage but lets an attacker probe whether a specific file already exists by timing or by skipped uploads. Many systems dedupe only within a user or organisation, or combine it with client-side encryption.

How do you keep version history affordable?

Versions share unchanged blocks, so each version costs only the blocks that differ. Old blocks move to a colder storage tier, and a reference-counting job deletes blocks no version uses once retention expires.

What happens if blocks upload but the commit never arrives?

The blocks have no references in any version, so they are orphans. A garbage collector deletes blocks whose reference count has been zero for longer than a grace period.