Dropbox
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Layer 4 · Full breakdown · 35 min · 12 figures

    Design Dropbox / Google Drive

    Design a file storage and sync service like Dropbox or Google Drive: chunked resumable uploads, block-level dedup, a per-namespace journal with cursors, offline conflict handling, sharded metadata and exabyte-scale block storage.

    A file sync service puts a magic folder on every device you own. Drop a 2 GB wedding video into it on your laptop in Pune and, a few minutes later, it is on your phone, your office desktop and your cousin’s laptop in Toronto. Under that promise sit three hard problems. Moving bytes efficiently: files range from a 2 KB note to a 50 GB video, Wi-Fi drops, and a one-line edit should not re-upload the whole file. Keeping devices in agreement: laptops go offline for days and edit the same file, yet every device must converge without silently losing anyone’s work. And storing exabytes for years at a cost that works when most data is never read again. This walkthrough designs Dropbox / Google Drive-style sync for desktop, mobile and web, leaning on what Dropbox has published about its own system.

    Understand the problem

    File sync looks like “object storage with a folder UI”, and weak answers stop there. Storing a file is easy; keeping hundreds of millions of devices — many offline at any moment — in agreement about which version lives at which path, while moving few bytes, is hard. Dropbox’s engineers describe network partitions, an anomaly for most distributed systems, as “standard operation” for a sync engine: a laptop on a long train journey is a partition that lasts all day.

    Functional requirements

    1. Users should be able to upload and download files of any size — from a 2 KB text file to a 50 GB video — from desktop, mobile and web clients, and resume after a dropped connection.
    2. Users should be able to have their files sync automatically across all their devices: a change saved on one appears on the others, including on a device that was offline for a week.
    3. Users should be able to share files and folders with other people as viewers or editors, or through a link.
    4. Users should be able to see a file’s version history and restore an earlier version or a deleted file.

    Non-functional requirements

    • Durability above everything. A committed file is never lost or corrupted: at least eleven nines of annual durability per block, with copies in two or more regions.
    • Availability. 99.99% for the metadata and sync APIs (about 52 minutes a year). Reads of stored files survive a full region outage.
    • Sync latency. For a small edit (under 1 MB), other online devices see it within 2 seconds at p50 and 10 seconds at p99. Large files are bound by the user’s uplink; keep it busy.
    • Bandwidth efficiency. Never upload a block the server already holds for this user; send roughly the edit, not the file; lose at most one block when interrupted.
    • Scale. 500 million registered users, 100 million daily active, 250 million linked devices, 500 billion files and 5 EB of user data (worked out below).
    • Consistency. Metadata is strongly consistent within a namespace: commits to one folder tree are totally ordered and none silently overwrites another. Propagation to devices is eventual — seconds for an online phone, days for a laptop in a drawer.
    • Security. Every block read is authorised, data is encrypted in transit and at rest, and revoking a share bites within minutes.

    In the interview

    Ask three questions before drawing. “Is this a sync product — a folder on every device — or a web drive with real-time co-editing?” The second turns into a Google Docs problem. “What is the largest file, and how often are clients offline?” That decides chunking and resumability. “Do we deduplicate across users?” That sounds like a storage question but is really a privacy decision.

    Out of scope

    • Real-time co-editing inside a document. That needs operational transforms or CRDTs; see design Google Docs. We sync whole files.
    • Search, previews and thumbnails. They hang off the same change stream but are their own designs.
    • Billing, enterprise compliance and malware scanning. Separate systems with their own trade-offs.
    • End-to-end encryption. Covered as a follow-up, because it changes dedup, previews and search.

    The set-up

    Plan the approach

    Say the shape early: content and metadata are two different systems. Bytes go to a large, simple, immutable block store; everything that changes — names, folders, versions, sharing — lives in a small, strongly consistent metadata store, with a notification tier telling devices when to look. Satisfy the requirements one by one, then spend your time on the deep dives: chunking, big uploads, dedup, the sync protocol, conflicts, the metadata store and storage cost.

    Core entities

    • Block. Up to 4 MB of file content, named by the SHA-256 hash of its plaintext, compressed and encrypted at rest. Immutable: once written it never changes; it is only ever garbage-collected.
    • File revision. One version of one file: the ordered list of block hashes (Dropbox calls it a blocklist) plus size, modification time and author.
    • Namespace. The root of a tree of files. Every user has a root namespace, and every shared folder is its own namespace, mounted into each member’s tree.
    • Journal entry. One row per change in a namespace — add, edit, move, delete — stamped with a journal ID (JID) that goes up by one per namespace.
    • Device cursor. For each namespace a device follows, the last JID it has applied.
    • Share. Who has which role on which namespace or file, plus link tokens for “anyone with the link”.

    The two tables that matter most look like this:

    -- Both tables are sharded by ns_id: a namespace never spans two shards.
    CREATE TABLE file_journal (
      ns_id      BIGINT       NOT NULL,
      jid        BIGINT       NOT NULL,   -- 1, 2, 3 … dense within the namespace
      file_id    BIGINT       NOT NULL,   -- stable across renames and moves
      parent_id  BIGINT       NOT NULL,   -- folder that contains it
      name       VARCHAR(255) NOT NULL,
      op         VARCHAR(8)   NOT NULL,   -- add | edit | move | delete
      rev        BIGINT       NOT NULL,   -- per-file version number
      size       BIGINT       NOT NULL,
      blocklist  MEDIUMBLOB,              -- 32-byte hashes, concatenated
      author_id  BIGINT,
      created_at TIMESTAMP,
      PRIMARY KEY (ns_id, jid)
    );
    
    CREATE TABLE file_latest (              -- current state: one row per live file
      ns_id      BIGINT NOT NULL,
      file_id    BIGINT NOT NULL,
      parent_id  BIGINT NOT NULL,
      name       VARCHAR(255) NOT NULL,
      rev        BIGINT NOT NULL,
      jid        BIGINT NOT NULL,
      blocklist  MEDIUMBLOB,
      PRIMARY KEY (ns_id, file_id),
      UNIQUE (ns_id, parent_id, name)       -- no two files at one path
    );

    A 50 GB file has about 12,500 blocks, so its blocklist is 400 KB of hashes. Store blocklists that large as a separate object and keep only its hash in the row.

    API

    Four groups of calls. Metadata goes through the API; block bytes never do.

    Commit a file. The client sends the blocklist it wants to commit, naming the revision it edited.

    POST /v1/files/commit
    Content-Type: application/json
    
    {
      "ns_id": 42,
      "path": "/Reports/q3-forecast.xlsx",
      "parent_rev": 17,
      "size": 9437184,
      "blocklist": ["9f2c…e1", "41ab…07", "c07e…5d"]
    }

    If some blocks are missing, the answer lists them with short-lived upload URLs:

    {
      "status": "need_blocks",
      "missing": [
        { "hash": "41ab…07", "upload_url": "https://blocks.example.com/b/41ab…07?exp=900&sig=…" }
      ]
    }

    Once they are uploaded, the same call succeeds — or reports that someone else committed first:

    { "status": "committed", "file_id": 88213, "rev": 18, "jid": 1042 }
    { "status": "conflict", "current_rev": 18, "your_parent_rev": 17 }

    Learn about changes. A device waits on a notification endpoint, then pulls journal rows after its cursor.

    GET /v1/notify/wait?cursors=42:1040,77:310&timeout=90
    { "changed": [42] }
    GET /v1/delta?ns_id=42&after=1040&limit=1000
    {
      "entries": [
        { "jid": 1041, "op": "move", "file_id": 88001, "path": "/Archive/old-plan.pdf" },
        { "jid": 1042, "op": "edit", "file_id": 88213, "rev": 18,
          "path": "/Reports/q3-forecast.xlsx", "blocklist": ["9f2c…e1", "41ab…07", "c07e…5d"] }
      ],
      "cursor": 1042,
      "has_more": false
    }

    Fetch a block. GET /v1/blocks/41ab…07 checks access and answers 302 to a signed URL on the block store or CDN, valid for a few minutes.

    Share and restore.

    POST   /v1/shares                     body: ns_id or file_id, invitee, role (viewer | editor)
    DELETE /v1/shares/{share_id}          revoke
    POST   /v1/links                      body: file_id, expires_at, password → link token
    GET    /v1/files/{file_id}/revisions  list versions: rev, size, author, time
    POST   /v1/files/{file_id}/restore    body: rev = 15 → new rev 19 that reuses rev 15’s blocklist

    Back-of-envelope estimates

    Assume 500 million registered users, 100 million daily active users and 2.5 devices per active user. (For the method, see back-of-envelope estimation.)

    Commits. 100M DAU × 10 file changes a day = 1B commits a day. 1B ÷ 86,400 ≈ 11.6k commits/s on average; three times that at the weekday-morning peak, ≈ 35k commits/s.

    Upload bandwidth. Suppose that after dedup, delta transfer and compression an average commit sends 1 MB of new data. 1B × 1 MB = 1 PB a day. 1 PB ÷ 86,400 s ≈ 11.6 GB/s, and × 8 ≈ 93 Gbps on average, ≈ 280 Gbps at peak.

    Download bandwidth. Each change reaches about 3 other devices — yours and your collaborators’. 3 PB a day ≈ 35 GB/s ≈ 280 Gbps on average, ≈ 840 Gbps at peak.

    Connections. 100M × 2.5 = 250M devices. If 60% are online at peak, that is 150M open notification connections: at 200k idle connections per server, 750 servers. If each long-poll is held for 90 seconds, 150M ÷ 90 ≈ 1.7M requests/s whose usual answer is “nothing changed”.

    Stored data. 500M users × 10 GB on average = 5 EB of logical data. New data arrives at 1 PB × 365 ≈ 365 PB a year, about 7% growth before deletions.

    Metadata. 500M users × 1,000 files = 500B files. With three revisions each, 1.5T journal rows — the order of magnitude Dropbox reports (hundreds of billions of files, trillions of revisions). At about 300 bytes a row, 1.5T × 300 B = 450 TB; with indexes and the latest-state table, call it 1 PB. At 2 TB per database shard that is ≈ 500 shards, and 35k commits/s ÷ 500 = 70 commits/s per shard on average. The average is trivial; the skew is the problem.

    Block index. Small files keep the average block well under 4 MB; say 2 MB. 5 EB ÷ 2 MB = 2.5T blocks, and at about 60 bytes per entry (hash, location, references), ≈ 150 TB of index. With 64 KB blocks it would be 5 EB ÷ 64 KB ≈ 76T entries, about 4.6 PB of index.

    Four of these numbers shape the design. Hundreds of gigabits of content means bytes must bypass the application servers. 150M mostly idle connections means notifications need their own tier. A petabyte of metadata that must be strongly consistent means sharding by namespace. And at 5 EB, every 0.1× of storage overhead costs 500 PB of disk.

    Tip

    Separate the three flows in a sync design: bytes (huge, immutable, cacheable anywhere), metadata (small, mutable, must be ordered) and notifications (tiny, frequent, disposable). Each wants a different system.

    High-level design

    Build it one requirement at a time; by the end it works for one region at moderate scale.

    Users should be able to upload and download files of any size

    The client splits each file into blocks of up to 4 MB, hashes each with SHA-256, and talks to the metadata service only in hashes. Dropbox’s 2014 description of its protocol is the template: the client tries to commit the blocklist; the server checks which hashes it has and whether the user may write, and answers “need blocks”. The client uploads those directly to the block store and commits again; now the server appends a journal row and the file exists.

    Step 1: upload and download. The client talks to the metadata service in hashes and moves bytes directly to and from the block store.Notice No file content passes through the metadata service, and the file exists only after the second commit succeeds.
    1. Laptop A commits the blocklist [h1, h2, h3] for /Reports/q3-forecast.xlsx, naming the revision it edited.
    2. The metadata service checks the block index and the user’s permission, and replies with the missing hash h2 and a signed upload URL valid for 15 minutes.
    3. The client PUTs the 4 MB block straight to the block store, which recomputes the hash and rejects a mismatch.
    4. The client repeats the commit.
    5. Now every block is present, so the service appends journal row JID 1042 and updates the file’s latest state in one transaction.
    6. Laptop B lists the journal after its cursor and sees the new row.
    7. It reuses blocks it already has locally, downloads the rest, verifies each hash, and renames the rebuilt file into place in one step.

    Why commit twice? The first commit is a question — “which of these do you lack?” — answered “all of them” for a new file and often “one of twenty-five” for an edit. Committing last means a file becomes visible only when every byte is stored, so no device ever downloads half a file. Mobile and web clients use the same calls; mobile apps hand block uploads to the OS background-transfer service so camera uploads survive the app closing.

    Users should be able to have their files sync across their devices

    A device needs to know that something changed and what changed. The journal answers the second: each device remembers, per namespace, the last JID it applied, so “catch me up” is “rows of namespace 42 after 1040”. For the first, add a notification service holding one long-lived connection per device that answers “namespace 42 changed” — nothing more. Dropbox’s client works this way: idle clients keep a long-poll to a notification server, then call list with their cursors.

    Step 2: a commit advances the namespace’s journal; a change stream wakes the notifier, which tells waiting devices to pull.Notice The notifier sends no file data at all — only “ns 42 changed”. The phone decides what to fetch from its own cursor.

    On the desktop, a file-system watcher (FSEvents, inotify, ReadDirectoryChangesW) reports local edits; the client waits until a file stops changing for a second or so, then hashes and commits.

    The key property: the journal is the truth, notifications are only hints. If a notification is lost or a phone sleeps through it, nothing breaks: on reconnect the device sends its cursors and learns it is behind. That is pull with a push hint, the robust middle of push versus pull.

    Users should be able to share files and folders

    Sharing breaks a path-based design. Asha’s folder /Team Q3 appears in Ravi’s tree as /Work/Q3 plan — a different path for the same files. So make the shared folder its own namespace with its own journal and JIDs, mounted into each member’s root namespace wherever they like. Every file is then identified by a namespace and a relative path, exactly as Dropbox describes its server-side file system.

    Step 3: a shared folder is a namespace of its own, mounted at a different path in each member’s tree, with one access list.Notice Asha and Ravi see the same files under different names, yet a change produces exactly one journal row, in ns 42.

    Three pieces make sharing work:

    • An access list per namespace (owner, editor, viewer), checked on every commit, every delta call and every block URL issued. Dropbox’s Edgestore models users and folders as entities joined by two-way associations, so “which folders can Ravi see?” and “who can see ns 42?” are both single lookups, and removing someone deletes one association. Sharing a single file is the same idea: an access-list entry on the file’s ID.
    • Link sharing. A link is a random token mapped to a file or namespace, a role, an expiry and an optional password. Opening it yields short-lived block URLs, never permanent ones.
    • Revocation that bites. Signed block URLs outlive the check that issued them, so keep them to 5–15 minutes. A removed member’s devices get a journal entry that unmounts the namespace and deletes the local copy.

    Users should be able to see version history and restore files

    Here content addressing pays off. An edit appends a journal row with a new blocklist; the old row stays, and the new version stores only blocks that did not exist before.

    Step 4: three revisions of a 16 MB spreadsheet. Each revision is a list of hashes; unchanged blocks are shared.Notice Restoring rev 17 uploads nothing: it is a new journal row pointing at blocks that are still stored.
    • List versions by reading the file’s rows from the journal (an index on ns_id, file_id, jid makes it one range scan).
    • Restore by committing a new revision whose blocklist is the old one. Every block is still stored, so the commit succeeds on the first call without uploading a byte.
    • Delete by appending a delete row. The file disappears from every device, but its last revision stays restorable.
    • Retention — for example 30 or 180 days, by plan — decides when old revisions expire. A garbage collector then deletes blocks that no live or retained revision references, after a grace period.

    You now have a working design: block uploads and downloads, a journal that orders every change, notifications that wake devices, namespaces for sharing, and cheap versions.

    Deep dives

    How should we split files into blocks?

    The block is the unit of upload, retry, dedup and storage. There are two families, and one trick that rescues the simpler one.

    Fixed-size blocks. Cut at every 4 MB boundary: block i covers bytes 4 MB × i up to 4 MB × (i + 1). This is what Dropbox does. It is simple, parallelises perfectly, and random access is arithmetic (offset ÷ 4 MB). The weakness is the boundary-shift problem: insert 10 KB of speaker notes near the start of a 100 MB presentation and every later byte moves 10 KB right, so all 25 blocks get new contents and new hashes. At the block level, the edit costs a 100 MB upload and 100 MB of new storage.

    The same 10 KB insert near the start of a file, cut into fixed blocks (top) and content-defined chunks (bottom).Notice Fixed boundaries are positions, so they all move; content-defined boundaries are patterns in the data, so they move with it.

    The rescue: rsync-style delta transfer. The rsync algorithm of Andrew Tridgell and Paul Mackerras sends only differences without either side holding both files. The side with the old version sends two checksums per block: a weak 32-bit rolling checksum and a strong hash. The side with the new version slides a window over its file at every byte offset; the rolling property updates the weak checksum in constant time per byte, and weak matches are confirmed with the strong hash. What crosses the wire is “copy old block 7 here” references plus literal bytes for what did not match, in one round trip. Applied per block, the shifted content is found at a 10 KB offset, so the 100 MB edit costs about 10 KB plus checksums. Dropbox’s 2014 write-up says its client used compression and rsync to shrink block transfers. The catch: rsync saves bandwidth, not storage — the 25 new blocks still have new hashes, so the store keeps 100 MB of new blocks.

    Content-defined chunking (CDC). Let the data choose the cut points. Slide a rolling hash over a 48-byte window and cut wherever its low 20 bits are all zero — on average once every 2²⁰ bytes, so chunks average 1 MB — with a minimum (256 KB) and maximum (4 MB) to bound the extremes. A cut depends only on the 48 bytes before it, so an insert changes the chunk containing it (occasionally its neighbour) while every later boundary moves with the data. The 100 MB edit now uploads and stores about 1–2 MB. The cost is CPU: the classic Rabin fingerprint does real arithmetic for every byte. The FastCDC paper (USENIX ATC 2016) uses the cheaper Gear hash, skips cut-point checks below the minimum size and normalises chunk sizes, and reports about 10× the speed of the best open-source Rabin-based CDC with nearly the same dedup.

    Fixed 4 MB blocks Fixed blocks + rsync delta Content-defined chunks (avg 1 MB)
    Bytes uploaded 100 MB ~10 KB + checksums ~1–2 MB
    New bytes stored 100 MB 100 MB (unless the store keeps deltas) ~1–2 MB
    Client CPU One SHA-256 pass Plus rolling checksums over old and new Plus a rolling hash per byte (cheap with Gear / FastCDC)
    Find byte offset X X ÷ 4 MB Same Needs a per-file offset table
    Dedup across files Only aligned copies Only aligned copies Shifted copies too
    Best for Simplicity, media files Sync of edited documents Backup, VM images, big binaries edited in place
    Three ways to cut files, judged on a 10 KB insert near the start of a 100 MB file

    Two more knobs. Block size trades index size against dedup granularity: 150 TB of index at 4 MB versus 4.6 PB at 64 KB, plus more requests per file. Compression shrinks what is left: Dropbox’s Broccoli (a Brotli encoder that compresses chunks in parallel) cut upload bandwidth by about a third and the median request from 3.5 MiB to 1.6 MiB. Skip already-compressed formats — more than a quarter of Dropbox’s data was incompressible, mostly video and images.

    The choice: fixed 4 MB blocks for storage and addressing, an rsync-style delta when re-uploading an edited block, and compression for compressible types — a simple server and most of the bandwidth win. Move to content-defined chunking if in-place edits to huge files dominate (VM images, database files, 2 GB Photoshop documents), or for a backup product where storage dedup across versions is the main cost lever.

    How do we upload a 50 GB file over a flaky connection?

    A 50 GB video over a 100 Mbps uplink takes 50 GB × 8 = 400 Gb ÷ 100 Mbps = 4,000 s, about 67 minutes. Over an hour on home Wi-Fi, something will go wrong.

    Bad: one big request through the application servers. A single PUT ties up a server for an hour, puts the 280 Gbps upload peak on servers built for small JSON requests, and any interruption restarts from byte zero.

    Good: a resumable session. Google Drive’s API shows the pattern: an initial request returns a session URI; after an interruption the client asks how much arrived and continues. Drive’s session URI expires after one week. Restarts are fixed, but the stream is sequential and the server keeps per-session state.

    Great: blocks in parallel, straight to the store, commit last. The block design you already have is a better resumable upload:

    1. The client hashes the file into about 12,500 blocks of 4 MB, then commits. The answer lists missing hashes with signed upload URLs, a page of 1,000 at a time.
    2. It uploads 4 to 8 blocks at once directly to the block store, keeping the pipe busy despite per-request latency. Each URL is bound to one hash and length for 15 minutes, and the store verifies the SHA-256 before acknowledging.
    3. When the connection drops, at most the blocks in flight are lost — 8 × 4 MB = 32 MB.
    4. After the reboot, the client simply commits again and learns what is still missing. There is no upload session: the stored blocks are the progress record.
    5. When nothing is missing, the commit succeeds and the 50 GB file appears on every device at once.
    Uploading a 50 GB file as 12,500 blocks, surviving a dropped connection and a reboot.Notice Recovery is just “commit again”: the server works out what is still missing from the blocks it already holds.

    Compare S3-style multipart upload, which you would use on a public cloud bucket. Parts must be 5 MiB to 5 GiB, at most 10,000 per upload, so a 50 GB file in 5 MiB parts needs about 9,540 — under the cap, just. Multipart gives parallelism and resume, but parts are anonymous pieces of one object, with no dedup across files or versions; content-addressed blocks give all three (more in large file uploads). Two refinements:

    • Streaming sync. Immutable blocks can safely be fetched before the commit. Dropbox let downloading clients prefetch blocks of a file still being uploaded, making large-file sync up to 2× faster; in their test a 500 MB file reached the second machine in 293 seconds instead of 383.
    • Orphaned blocks. A cancelled upload leaves unreferenced blocks. The garbage collector must not race a commit about to reference one: give blocks a grace period longer than the resume window (say 7 days), and have the commit re-check inside its transaction that each block still exists.

    Never trust the client’s hash

    The server must recompute the SHA-256 of every uploaded block, after decompression. Accept a client’s claim that some bytes are block h, and one buggy or malicious client poisons h for every file that uses it. Dropbox notes that clients rarely have error-correcting memory and memory corruption is a steady fact of life, so it always verifies the hash of the uncompressed block.

    Should identical blocks be stored once across all users?

    Within one user or team, deduplication is uncontroversial: if your account holds block h, never upload it again. Across all users it is tempting — the same film trailer or viral PDF stored once instead of a million times. The trouble is the client-side version, where the server says “someone already has that, skip the upload”. That answer is information.

    • An existence oracle. An attacker learns whether anyone stored a file by uploading it and watching whether the upload is skipped. With a template and a few thousand guesses — an offer letter with “Salary: ₹X” for every plausible X — they read the value inside someone else’s private document.
    • A hash becomes a key. If the server grants a block on its hash alone, anyone who learns a file’s hash can claim and download the file. Halevi, Harnik, Pinkas and Shulman-Peleg showed this attack and proposed proofs of ownership: Merkle-tree challenges a client can answer only if it holds the whole file.
    Global, client-side Per user or team on the client, global on the server None across users
    Upload saved for a popular file Yes No — each user uploads once No
    Disk saved for a popular file Yes Yes No
    Leaks whether a file exists Yes No No
    Hash alone grants access Yes, unless proofs of ownership No No
    Works with per-user encryption keys No Within the user or team only Yes
    Choosing the scope of deduplication

    The choice: deduplicate on the client only within a user or team, and across users only on the server, after the bytes arrive. Uploads reveal nothing about other people and you still keep one physical copy; you pay extra upload bandwidth for popular files, on the user’s uplink rather than your disks. The block server still checks on every read that the requester can reach a namespace referencing the block, as Dropbox’s does. With per-user encryption keys cross-user dedup is impossible; convergent encryption (key derived from content) permits it but inherits the existence leak.

    How do devices learn about changes quickly without melting the servers?

    150 million devices are online at peak, and nearly all of them, nearly all the time, have nothing new to learn.

    Bad: poll the full listing every 30 seconds and diff it: 150M ÷ 30 s = 5M listing requests a second, each possibly thousands of rows, and changes still take 15 seconds on average to appear.

    Good: poll with a cursor. delta?after=1040 every 30 seconds makes unchanged namespaces cheap, but it is still 5M requests a second at the metadata tier, almost all answering “nothing”, with the same delay.

    Great: a notification tier plus cursor deltas. Each device keeps one long-poll (or WebSocket) to a notifier, listing its namespaces and cursors; notifiers subscribe to those namespaces on an internal pub/sub. When a commit lands, the change stream — fed by change data capture or a transactional outbox, so no committed row is missed — publishes “ns 42 is at 1042”, and the notifier answers every waiting device whose cursor is behind. Only then does a device call the metadata service, for the namespaces that moved.

    The notification path for one small edit: a hint travels to the phone, and the phone pulls exactly what it is missing.Notice The notifier and the metadata service never talk about file contents, and the phone calls the metadata service only after it knows something changed.

    The budget for a small edit: change stream about 100 ms, notifier 50 ms, delta call 50 ms, a 1 MB block from a nearby edge 100–200 ms — roughly 0.5 s at p50, inside the 2-second target.

    Why not push the file contents? Many devices are offline, so pushing means queueing content per device for days. Devices want different things: a phone fetches content on tap, a laptop with selective sync wants some folders. A pull from a cursor is idempotent and resumable for free; a push stream needs acknowledgements and redelivery. And only the device knows which blocks it already holds. Google Drive’s public API has the same shape: a start page token as the cursor, a changes list after it, and push notifications that, in the Drive documentation’s words, “don’t contain details about the changes”.

    Three operational details:

    • The long-poll timeout sets your request rate. A 90-second hold costs the 1.7M requests a second from the estimates; 30 seconds would triple it. A WebSocket with a 5-minute heartbeat costs far less, but a notifier restart then causes a reconnect storm — spread it with jittered backoff (see long polling, SSE and WebSockets).
    • Fan-out from big shared folders. A commit in a 20,000-member folder wakes 20,000 × 2.5 = 50,000 devices, all calling delta on one namespace. Coalesce on the notifier (one wake-up per device per second), and cache: journal rows are immutable, so a page “ns 42, JIDs 1041–1042” can be served 50,000 times.
    • Cursor too old. A laptop back after six months may hold a cursor older than the journal kept online. The server answers “reset”; the client fetches a full listing and reconciles it with the disk.

    How do we handle conflicts when devices edit offline?

    Asha edits budget.xlsx on a flight; Ravi edits the same file in the office. Both started from revision 17. Ravi’s commit becomes revision 18. Asha lands and her laptop commits — also claiming revision 17 as its parent.

    Bad: last writer wins by timestamp. Device clocks drift by minutes, and either way one person’s afternoon of work silently disappears — the one unforgivable outcome.

    Good: compare-and-set on the parent revision, and a conflicted copy for the loser. Every commit names the revision it edited, and is accepted only if that is still the latest — optimistic concurrency inside the namespace’s transaction. The loser keeps its version as a new file, budget (Asha’s conflicted copy 2026-10-27).xlsx, and downloads revision 18 under the original name. Nothing is lost; a human merges, because files are opaque bytes to us.

    Two devices edit revision 17 of the same file; the server orders the commits and the loser becomes a conflicted copy.Notice The server never compares clocks: it only checks whether the revision a commit was based on is still the latest.

    Great: a client data model that always knows which side changed. The hard half is on the device. When the laptop sees /qux on the server but not on disk, was it added remotely (download) or deleted locally (delete on the server)? Guess wrong and you resurrect files the user deleted or delete files a colleague just added. Dropbox’s rewritten sync engine, Nucleus, persists three trees:

    • the remote tree — the latest server state it has seen;
    • the local tree — the last observed state of the disk;
    • the synced tree — the last state where the two agreed, playing the role of a merge base in version control.
    The three trees for one folder, and the plan derived from them.Notice The bottom line: with only the remote and local trees, a file deleted here and a file added elsewhere look identical. The synced tree tells them apart.

    The rule per node: local equals synced but remote differs — download. Remote equals synced but local differs — upload. Both differ (and from each other) — a conflict, resolved as above. A planner turns the differences into batches of operations safe to run concurrently; sync is done when all three trees are equal.

    Two more rules worth stealing from that rewrite. Key nodes by a stable ID, not by path. The old engine stored a move as a delete plus an add; when a network blip let the delete through but not the add, the file vanished from other devices although the user had only moved it. With IDs, a move is one attribute change and one atomic rename on disk. Design invalid states away — no node may exist without its parent, even briefly. The state space is so large that Dropbox runs tens of millions of seeded, deterministic randomized tests on its planner every night. Other races, and sensible rulings:

    Race What the server sees Ruling
    Edit on one device, delete on another Edit with parent 17 after a delete, or the reverse Keep the edit: a delete is easy to redo, lost work is not.
    Delete a folder while someone adds a file inside it Add into a deleted parent Re-create the folder holding only the new file.
    Two renames of the same file Second commit names a stale revision First wins; the second device re-plans from the new state.
    Move A into B while B moves into A Second move would create a cycle Reject the second move; that device re-plans (Dropbox’s Archives, Drafts and January example).
    Report.txt and report.txt Fine on Linux, a collision on Windows and macOS Rename the later one locally as a conflicted copy.

    In the interview

    Say it plainly: “Offline is the normal case, not an edge case. I never resolve conflicts with device clocks. The server orders commits per namespace, every commit names the revision it edited, and the loser becomes a conflicted copy, so no path ever loses bytes silently. On the client, three trees — remote, local and a synced merge base — tell me which side made each change.”

    How do we scale the metadata store?

    About 1 PB, 35k commits a second at peak, many times that in reads, strongly consistent within a namespace. Dropbox built this on sharded MySQL; its Edgestore layer reports several trillion entries and millions of queries per second, strongly consistent by default with caches invalidated on writes. The shard key decides everything.

    Bad: shard by file ID or path hash. A folder scatters across shards, so checking that a name is free and allocating the next JID becomes a distributed transaction on every save, and listings become scatter-gather.

    Good: shard by user. A user’s root namespace lives together — but a shared folder belongs to many users, so whose shard holds it?

    Great: shard by namespace. A namespace’s journal, latest state and JID counter live on one shard, so a commit is a single-shard ACID transaction: check parent_rev, check the name is free, insert with JID = last + 1, update the latest state. JIDs come out dense and ordered with no global coordination. A small, cached shard map (namespace → shard) lets you move one hot namespace to its own shard without rehashing anything else — a directory rather than a hash function (see database sharding).

    Metadata sharded by namespace: thousands of small namespaces share a shard, one huge shared folder gets a shard to itself.Notice Only commits must reach the primary. Because journal rows never change, a lagging replica returns a correct, shorter prefix — safe to read.

    Hot namespaces. The skew is a 20,000-member company folder. Its writes are rarely the problem — one primary handles thousands of small transactions a second. Its reads are: each commit triggers up to 50,000 delta calls. Two properties save you. Journal rows are immutable, so pages can be cached forever by (ns, JID range). And a lagging replica still returns a correct prefix of the log: asked for rows after 1040, a replica at 1041 returns 1041 and cursor 1041, and the device asks again. So reads go to replicas and caches safely; only the compare-and-set needs the primary. Give the biggest tenants a cell of their own, so one enterprise’s Monday morning cannot slow everyone else.

    Cross-namespace moves. Dragging a file from a private folder into a shared one touches two shards. Rather than two-phase commit, run a small saga with a move ID: add to the target, then delete from the source, each step idempotent. A crash in between leaves the file in both places, never in neither — a duplicate is an annoyance, a loss is a crisis.

    Journal growth. Keep the latest state and recent journal online; archive rows older than the retention window, which is why very old cursors get “reset”.

    How do we store exabytes durably without going broke?

    The block store holds 5 EB and grows by a third of an exabyte a year, so its overhead factor is the infrastructure budget. Dropbox’s Magic Pocket is a well-documented example.

    • Immutable, content-addressed blocks of up to 4 MB are packed into 1 GB buckets, each stored in a volume spread over several machines; a block index — a large sharded MySQL cluster, not an exotic key-value store — maps each hash to its cell and bucket.
    • Two zones at least. Each block reaches a second region within about a second. Inside a region, independent cells of about 50 PB limit the blast radius of a bug.
    • Replicate first, erasure-code later. New data is replicated — cheap to write, fast to read and repair; older volumes are erasure-coded in the background, laid out so a block can normally still be read from one disk.
    • Repair quickly but not nervously. A node offline for 15 minutes triggers repair, and hundreds of machines rebuild its scattered volumes in parallel.

    Ageing makes the economics work: Dropbox measured that over 40% of retrievals are for data uploaded in the last day, over 70% in the last month and over 90% in the last year. Most bytes are cold and can sit in the cheapest encoding the durability rules allow.

    Raw disk needed to keep 5 EB of user data under three storage schemes.Notice The encoding choice moves the bill by tens of exabytes — far more than any dedup or compression trick.

    The 10+4 code is illustrative (ten data and four parity fragments: survives any four losses at 1.4×); the cross-region scheme is Dropbox’s. Its cold tier splits each block into two fragments in two regions and stores their XOR in a third; reads ask all three and use the fastest two, so a whole region can be down unnoticed. The cross-region factor drops from 2 to 1.5 — a 25% cut in disk usage (33% with four regions and 3+1). Cold reads pay about one extra cross-region round trip, yet their p99 came out lower than the warm tier’s, because racing three requests hides a slow one.

    Full replication Erasure coding inside a region Fragments across regions (cold)
    Disk overhead (with 2+ regions) 6× for 3 copies in 2 regions ~2.8× ~2.1×
    Write cost Write N copies Encode, write k + m fragments Async migration; writes can pause
    Normal read One disk, any copy One disk if the block sits in one fragment Two regions, cross-region round trip
    Read during a failure Another copy Decode from k fragments Unchanged: always fastest 2 of 3
    Repair traffic Copy one replica Read k fragments per lost one Cross-region
    Best for Fresh, hot data Warm data Data untouched for months
    Ways to protect blocks, and what each costs

    One lesson is worth quoting in an interview. Dropbox first built a single erasure code spanning all regions and abandoned it after more than nine months: one software instance across every region meant one bug could destroy every copy. Independent failure domains are worth paying for; mathematical nines are not real ones. Deletion costs too: blocks are shared across files and versions, so the garbage collector must prove a block unreferenced (reference counts, double-checked by periodic mark-and-sweep) and compact the freed space in closed buckets. See cost-aware design.

    The final design

    Put the pieces together. The metadata plane is small and strongly consistent; the block plane is huge and dumb; the notifier and change stream connect them without carrying any content.

    The complete design: clients talk to the metadata plane in hashes, move bytes directly to the block plane, and are woken by the notifier.Notice File content never crosses the metadata plane, and the only thing flowing from metadata to devices is “namespace N is at JID J”.

    Follow one edit from Asha’s laptop to Ravi’s phone:

    1. Commit. Asha saves q3-forecast.xlsx; her client hashes its 4 MB blocks and commits the blocklist through the API gateway, naming parent revision 17.
    2. Check. The metadata service routes by namespace, checks Asha’s role on ns 42 and the block index, and returns the missing hashes with signed upload URLs.
    3. Upload. The client PUTs missing blocks — delta-encoded and compressed where it helps — straight to the warm block store, which verifies each SHA-256 and writes it to two regions.
    4. Append. The client commits again. One single-shard transaction checks that revision 17 is still the latest, appends JID 1042 and updates the latest state; if the check fails, the client keeps a conflicted copy.
    5. Publish. The committed row enters the change stream as “ns 42 @ 1042”.
    6. Notify. Notifiers answer long-polls waiting on ns 42, one coalesced wake-up per device.
    7. Pull. Ravi’s phone calls delta after its cursor (served by a replica or cached journal pages), GETs only the blocks it lacks, verifies them and updates its three trees.
    8. Background. Workers on the same stream garbage-collect unreferenced blocks after retention, move cold volumes to the cross-region tier, and feed search and previews.

    Check it against the requirements. Durability: blocks are verified on arrival, in two regions within about a second, later erasure-coded across three, and never deleted while a retained revision references them. Availability: reads survive a region loss; notifier and block store degrade independently. Sync latency: about half a second for a small edit, inside the 2-second p50. Bandwidth: only missing blocks travel, edits as deltas, and a dropped upload loses only blocks in flight. Scale: content bypasses the 500 metadata shards, and 150M connections live on their own tier. Consistency: commits are totally ordered per namespace with compare-and-set; conflicts produce copies, never silent loss.

    What interviewers look for by level

    Area Mid-level Senior Staff
    Scope Upload, download, share Separates content, metadata and notifications; sets durability and sync-latency targets Treats offline as the normal case; raises the privacy and cost questions unprompted
    Upload path Whole files through the API Chunked, hashed, resumable, direct to storage with signed URLs Commit-last with no session state, server-side hash checks, orphan GC that cannot race a commit
    Chunking and dedup Not discussed Fixed blocks, dedup by hash Fixed versus content-defined chunking, rsync deltas, dedup scope and its side channels
    Sync protocol Clients poll Journal with cursors plus push notifications Notifications as hints, long-poll cost, fan-out for big folders, cursor reset, prefix-safe replica reads
    Conflicts Last write wins Parent-revision check and conflicted copies Three trees with a merge base, stable IDs, move and delete races, how to test it
    Metadata scale One SQL database Shards by user or namespace Namespace sharding with a directory, dedicated cells for hot tenants, cross-namespace sagas
    Storage cost “Put it in S3” Copies in several regions Replicate then erasure-code, a cross-region cold tier, independent failure domains, safe GC

    Follow-up questions

    What if users want end-to-end encryption?

    The server must never see plaintext or keys. Each folder gets a random content key, wrapped with each member’s public key; sharing wraps it for one more person, and revoking rotates to a new key for future writes (old blocks stay readable to whoever had the old key). Block names can no longer be plain SHA-256 of the plaintext — that reveals when two users hold the same file — so use a keyed hash per folder, limiting dedup to the folder or team. Server-side previews and search stop working unless clients produce them encrypted; delta sync still works because it runs on the client. The trade: much stronger privacy for less dedup, fewer server features and painful key recovery (see encryption and key management).

    What if ransomware encrypts every file on someone’s laptop?

    To the sync engine it looks like a user editing 40,000 files in five minutes, which it will dutifully upload. Detect: a per-device anomaly check on the commit stream — a spike of edits and renames whose new contents look random (incompressible) where the old ones compressed well — pauses that device and alerts the user. Recover: every revision is a blocklist and old blocks are retained, so rolling a namespace back to “Tuesday 10:00” is metadata-only: for each file changed since, commit a revision reusing the older blocklist. Restoring 40,000 files moves no bytes on the server.

    What if a whole region goes down?

    Blocks are already in a second region, so reads keep working. For metadata, promote the surviving region’s replicas — which, with asynchronous replication, may have lost the last few commits. The danger is on the client: a laptop whose synced tree contains a lost commit sees “remote differs from synced, local unchanged” and concludes the file was deleted remotely. So clients must treat a namespace whose JID goes backwards as a server rollback and re-upload local state instead of applying deletions. If no rollback is acceptable, make commits wait for a replica in another region, paying tens of milliseconds each — a PACELC trade made knowingly (see multi-region and active-active).

    What if 200 people in one office need the same 2 GB file?

    Without help, the office link carries 200 × 2 GB = 400 GB. With LAN sync, clients announce themselves by local broadcast and ask each other for blocks by hash before going to the internet, so the file crosses the WAN roughly once and spreads at LAN speed. Content addressing makes untrusted peers safe — every block is verified against its hash — but a peer should serve a block only to a device holding a short-lived server token for a namespace containing it. See peer-to-peer architecture.

    Don’t replay the journal from JID 1. Fetch a snapshot listing of the latest state tagged with the namespace’s current JID, paginated and compressed (2 million entries at about 300 bytes is 600 MB raw, so 10,000 per page), then switch to normal deltas. Create folders before files and prioritise recently used content. For huge accounts default to online-only placeholders: every file shows its name and size, and content downloads when opened — so a 2 TB team archive does not fill a 512 GB laptop. Client memory per tracked file, which Dropbox set out to improve in its rewrite, then matters as much as bandwidth.

    How to say it in the interview

    “I’ll design file sync like Dropbox or Google Drive: 100 million daily users, about 250 million devices, 5 exabytes of data, and one rule above all — a committed file is never lost.

    The key decision is to separate content from metadata. Files are cut into blocks of up to 4 megabytes, named by their SHA-256 hash, in an immutable block store. A file version is an ordered list of block hashes. The client commits that list; the metadata service replies with the missing blocks and signed URLs; the client uploads them in parallel straight to the block store and commits again. A file appears only when every block is present, and a dropped upload resumes by committing again.

    Metadata lives in a strongly consistent store sharded by namespace — each user’s root and each shared folder is a namespace — so a commit is a single-shard transaction that appends a journal row with the next journal ID. Devices keep a cursor per namespace. A notification tier holds about 150 million long-polls and only ever says ‘namespace 42 changed’; devices then pull the rows after their cursor and fetch the blocks they lack. Notifications are hints; the journal is the truth.

    For conflicts, every commit names its parent revision. If that is stale, the device keeps its version as a conflicted copy, so no edit is lost. On the client I keep three trees — remote, local and the last synced state — so I always know which side changed.

    For cost and privacy, I deduplicate on the client only within a user or team, and across users only on the server, so uploads never reveal what other people store. Fresh blocks are replicated in two regions; cold blocks move to erasure-coded fragments across three regions, which cuts disk by about a quarter. Big shared folders read from replicas and cached journal pages, because journal rows never change.”

    Concepts used in this design

    References and further reading

    1. Streaming File Synchronization (Dropbox Tech Blog, 2014)dropbox.tech4 MB blocks named by SHA-256, blocklists, namespaces, the Server File Journal with per-namespace journal IDs, the commit / need-blocks protocol, long-poll notifications and the up-to-2× streaming sync result.
    2. Inside the Magic Pocket (Dropbox Tech Blog, 2016)dropbox.techImmutable 4 MB blocks packed into 1 GB buckets, a sharded MySQL block index, two-zone replication within a second, ~50 PB cells, replication then erasure coding, and repair after 15 minutes.
    3. How we optimized Magic Pocket for cold storage (Dropbox Tech Blog, 2019)dropbox.techFile access by age (40% in a day, 70% in a month, 90% in a year), the 2+1 cross-region fragment scheme that cut disk use 25%, fastest-2-of-3 reads, and why a single cross-region code was abandoned.
    4. Rewriting the heart of our sync engine (Dropbox Tech Blog, 2020)dropbox.techWhy offline is normal for sync, the move-as-delete-plus-add durability bug, the directory-cycle example, and the scale: hundreds of billions of files and trillions of revisions.
    5. Testing sync at Dropbox (Dropbox Tech Blog, 2020)dropbox.techThe three-tree model (remote, local, synced as merge base), stable node IDs instead of paths, the planner, and deterministic randomized testing.
    6. Broccoli: Syncing faster by syncing less (Dropbox Tech Blog, 2020)dropbox.techBrotli-compressed block transfer: ~33% less upload bandwidth, p50 request size 3.5 MiB to 1.6 MiB, and why the server re-hashes the uncompressed block.
    7. Reintroducing Edgestore (Dropbox Tech Blog, 2016)dropbox.techA strongly consistent, cached, sharded metadata store on MySQL; sharing modelled as user–folder associations.
    8. The rsync algorithm (Tridgell and Mackerras, ANU technical report)rsync.samba.orgWeak rolling plus strong checksums per block, matching at every byte offset, and sending only unmatched literal data in one round trip.
    9. FastCDC: a Fast and Efficient Content-Defined Chunking Approach (USENIX ATC 2016)usenix.orgGear-based content-defined chunking with cut-point skipping and normalised chunk sizes: about 10× faster than Rabin-based CDC with nearly the same dedup ratio.
    10. Proofs of Ownership in Remote Storage Systems (Halevi, Harnik, Pinkas, Shulman-Peleg, 2011)eprint.iacr.orgAttacks on client-side cross-user dedup: knowing a file’s hash is enough to claim and download it; Merkle-tree proofs of ownership as the fix.