14. Distributed File Storage (Dropbox) Advanced + Storage & Sync
A. Realistic Interview Prompt
The naive framing of this problem — "store files, sync them to other devices" — hides the one detail that actually makes it hard: files are often large, edits to them are usually small (a few paragraphs in a huge document, a handful of frames in a video project), and devices are not reliably connected at the same time. Re-sending an entire file for every tiny edit, from every device, is the honest failure mode this problem is built to test whether a candidate avoids. The load-bearing decision this design is built around is: split every file into content-defined chunks using a rolling hash, so an edit only changes the handful of chunks actually touched (surviving insertions and deletions, unlike naive fixed-size slicing); store those chunks in a global, content-addressable, deduplicating chunk store, separate from a small metadata service that tracks each file's version history as an ordered chunk list; detect concurrent edits across devices with per-device version vectors rather than a single always-online ordering authority; and — because arbitrary file content has no composable edit semantics the way text operations do — resolve genuine conflicts by keeping both versions as a "conflicted copy" instead of attempting to automatically merge them.
B. Difficulty
Advanced — every individual mechanism (chunking, a metadata/blob split, version vectors) is independently well-understood, but the problem is genuinely hard to get right end to end because it demands recognizing what does not transfer from adjacent problems: the OT/CRDT machinery that solves concurrent editing for Google Docs (P9) does not apply here, and a candidate who reaches for it anyway — without noticing why it doesn't apply — is the most common way this problem goes wrong at the senior level.
C. What This Problem Tests
- Recognizing that "sync the file" actually means "sync only the bytes that changed," and reaching for content-defined chunking rather than fixed-size slicing or whole-file transfer as the mechanism that makes that possible
- Applying the metadata/blob store split (Patterns Playbook D, Prerequisites I) a second time in this document with a twist: the "blob" here is itself further split into content-addressed chunks, which is what turns a metadata/blob split into a genuine deduplication mechanism rather than just a storage-cost optimization
- Detecting concurrent modification without a single, continuously-available ordering authority — the opposite structural assumption from Google Docs's per-document coordinating server (P9, Deep Dive M) — because devices here are routinely and expectedly offline for hours or days, not just briefly disconnected
- Real judgment on why two adjacent-looking "collaborative state" problems (Google Docs's text edits, Dropbox's file edits) demand two fundamentally different conflict-resolution strategies, tied concretely to whether the underlying edits are composable or not — not just naming "OT" and "conflicted copy" as two options to pick between arbitrarily
- Treating offline editing as the normal case to design for from the start, not a rare edge case bolted on afterward, which reshapes the notification/sync path (S) and the conflict-detection mechanism (P) alike
D. Requirements
Functional
- A user can upload, edit, delete, and organize files inside one or more synced folders
- Edits made on one device propagate to all of that user's other devices, without requiring the entire file to be re-transferred for a small edit
- A device that was offline when an edit happened correctly catches up once it reconnects
- If two devices edit the same file while both are disconnected from each other, the system detects that conflict rather than silently discarding one side
- (Extension, addressed in AA) shared folders across multiple users follow the same underlying mechanics as multiple devices for one user
Non-Functional
- Bandwidth-efficient sync — the cost of syncing an edit should scale with the size of the edit, not the size of the file it was made to
- Storage-efficient at rest — identical content, whether from repeated versions of the same file or entirely unrelated files across different users, should not be stored as duplicate bytes
- Offline-tolerant by default — a device with no network connection must still let the user keep working locally, with sync resuming automatically and correctly on reconnect
- No silent data loss — a legitimate edit from any device must never simply vanish because it collided with another edit; the honest failure mode when two edits genuinely conflict is surfacing the conflict, never discarding a side
- Durable — once a device is told an edit has synced, that content must survive individual machine failures in the storage layer
Out of scope: real-time, keystroke-level collaborative co-editing of a single document's contents (that is Google Docs's problem, P9, not this one); fine-grained sharing permissions and link-based public sharing; client-side end-to-end encryption key management.
E. Clarifying Questions
| # | Question | Why It Matters | Design Decision Affected |
|---|---|---|---|
| 1 | Are we syncing arbitrary file types (binaries, videos, archives) or specifically structured documents we could parse and diff semantically? | Determines whether the sync layer can ever assume anything about a file's internal structure — if it must handle arbitrary bytes, chunking has to be content-agnostic | Deep Dive 1 (M), Deep Dive 5 (Q) |
| 2 | How many devices does a typical user sync across, and are simultaneous edits from two devices to the same file expected to be common or rare? | Shapes how much weight the conflict-detection path (P) needs to carry versus being a rare-case safety net | Capacity Estimation (G), Deep Dive 4 (P) |
| 3 | Must offline editing be a first-class supported case, or can we assume devices are usually connected? | If offline periods are expected to be routine and long, the sync/notification design (R, S) has to treat "device hasn't been heard from in days" as normal, not exceptional | Deep Dive 6 (R), Deep Dive 7 (S) |
| 4 | Is cross-user deduplication (two unrelated users storing the same file) in scope, or only per-user version deduplication? | Determines whether the chunk store needs to be one global, shared, content-addressed space, or can be scoped per user | Deep Dive 2 (N), Deep Dive 3 (O) |
| 5 | What's a realistic distribution of file sizes — mostly small documents, or do we need to design around large media/project files too? | Directly drives chunk size choice and whether large-file parallel transfer needs to be a first-class concern | Capacity Estimation (G), Deep Dive 1 (M) |
F. Assumptions
- Confirmed: multi-device sync of arbitrary files, offline-tolerant, with conflicts surfaced rather than silently dropped.
- Assumption: files are arbitrary, opaque byte sequences to the sync layer (E's clarifying question 1) — the sync engine never assumes it understands a file's internal structure, which is the assumption that makes content-defined chunking (M) content-agnostic and rules out semantic/text-aware merging (Q) as a general solution.
- Assumption: a typical user syncs across a handful of devices (laptop, phone, tablet), and genuinely concurrent edits to the same file from two of them, while real, are the minority case rather than the common one (E's clarifying question 2) — conflict handling (P, Q) is designed to be correct and safe in that minority case, not optimized as if it were the dominant traffic pattern.
- Assumption: offline editing is routine, not exceptional (E's clarifying question 3) — a laptop closed overnight or a phone with no signal for a day are both normal operating conditions this design must handle gracefully, not degraded edge cases.
- Assumption: deduplication is global, across all users, not just across a single user's own file versions (E's clarifying question 4) — this is what makes the chunk store's content-addressing genuinely valuable at scale, versus being a narrower per-user optimization.
- Assumption: file sizes span from small documents to multi-gigabyte project/media files (E's clarifying question 5) — large files are treated as a first-class case throughout, not an afterthought.
G. Capacity Estimation
| Metric | Value / Calculation | Why It Matters |
|---|---|---|
| Registered users | Illustratively, ~500M | The baseline scale driver for every other row in this table |
| Files per user, platform-wide total | ~2,000 files/user average (F) → ~500M × 2,000 ≈ 1 trillion files | Sets the scale of the metadata layer — a trillion small records, not a trillion large ones |
| Total logical data footprint | ~1T files × ~2MB blended average size ≈ ~2 exabytes, before any deduplication | This is what users collectively believe they're storing — the number the chunk store's dedup (N) is measured against reducing |
| Average chunk size (content-defined) | Target average ~4MB, allowed to range ~2-8MB depending on where content-defined boundaries actually fall (M) | Big enough to keep chunk-count and per-chunk metadata overhead manageable; small enough that a typical edit only invalidates a handful of chunks, not the whole file |
| Unique chunks physically stored after cross-user dedup | ~2EB / 4MB ≈ 500B chunk references logically, but common files (OS installers, shared templates, duplicate uploads) mean the physically unique bytes stored is meaningfully lower — illustratively, a 20-30% reduction, landing physical storage in the ~1.4-1.6EB range | The concrete number that makes global, cross-user content-addressed dedup (N) a real cost lever, not a hypothetical one |
| Sync-triggering edits/day | ~150M daily active users × ~15 save/autosave events/day ≈ ~2.25B sync commits/day ≈ ~26,000/sec average | The write volume the metadata/commit path (K, P) must sustain — orders of magnitude smaller than the chunk byte volume, reinforcing why metadata and chunk bytes are scaled completely separately (O) |
| Metadata footprint | ~1T files × ~3 retained versions avg × ~300 bytes/version row (manifest pointer + vector) ≈ roughly 900TB-1PB | Large in absolute terms, but three orders of magnitude smaller than the chunk-byte footprint — confirms metadata and chunk bytes need entirely different storage and scaling strategies (O) |
| Per-sync latency budget | Low seconds, not sub-100ms — users expect sync to happen "in the background, soon," not at typing speed | A dramatically looser budget than Google Docs's ~150-200ms (P9) or Autocomplete's sub-100ms (P13) — this system is optimized for bandwidth and storage efficiency, not raw latency |
H. Core Entities
| Entity | Purpose | Key Fields |
|---|---|---|
File | One logical file path owned by a user/workspace — the stable identity a device syncs against | file_id, owner_id, path, current_version_id, deleted |
FileVersion (manifest) | One committed version of a file's content — the actual unit synced between devices (L, N) | version_id, file_id, chunk_hashes (ordered list), size, committing_device_id, version_vector, created_at |
Chunk | One content-addressed block of bytes, the smallest unit ever transferred or deduplicated (M, N) | chunk_hash (primary key, a content hash), size, ref_count |
Device | One registered client instance for a user, the unit sync state and notifications are tracked per (S) | device_id, user_id, last_synced_version per watched file, push_token/poll endpoint |
ConflictedCopy | A derived File-like record created when two devices' version vectors are found to be genuinely concurrent (P, Q) | original_file_id, conflicted_version_id, created_by_device_id, resolved |
I. API Design
-- client-facing:
POST /files/{file_id}/sync-check { local_manifest_version }
-> { needs_upload, needs_download, missing_chunk_hashes[] } (L, N)
PUT /chunks/{chunk_hash} -> 201 Created, or 204 if already known (dedup no-op) (N)
GET /chunks/{chunk_hash} -> chunk bytes (N)
POST /files/{file_id}/commit { chunk_hashes[], based_on_vector }
-> { new_version_id, new_vector } | 409 Conflict (P, Q)
GET /files/{file_id}/manifest?version={id} -> ordered chunk_hash list (L)
GET /devices/{device_id}/changes (long-poll) -> [{ file_id, new_version_id }] (S)
-- internal, cluster-facing:
(GC worker) periodic: chunk ref_count reaches 0 -> reclaim chunk bytes (O)
(conflict resolver) on 409 -> materializes ConflictedCopy + notifies the losing device (Q)
Why chunk upload/download are separate endpoints from the metadata commit itself: the metadata service handling sync-check and commit only ever touches small JSON payloads — hashes, vectors, version IDs — while PUT/GET /chunks/{hash} carry the actual large byte payloads, and can be served through pre-signed, direct-to-blob-store URLs exactly the way Prerequisites I's pre-signed URL pattern keeps large transfers off the application tier in Video Streaming's upload pipeline (Prerequisites I). Keeping these paths separate is what lets the metadata service stay small, fast, and cheap to scale independently of raw chunk traffic (O).
Why sync-check exists as its own call rather than folding into commit: a device needs to know which chunk hashes the server has never seen before it uploads anything — uploading blindly and letting the server discard duplicates on arrival would still waste the exact bandwidth this whole design exists to save (N).
J. Basic / Naive High-Level Design (V1)
The simplest thing that satisfies the functional requirement in isolation: whenever a file changes on any device, upload the entire file as one object, overwriting whatever was previously stored; every other device periodically checks whether the file's timestamp changed and, if so, downloads the entire new file in full.
(saves file)"]) -- "entire file bytes" --> API["Sync API"] API -- "overwrite whole object" --> Store[("Blob Store")] B(["Device B
(polls for changes)"]) -- "GET whole file" --> API API -- "entire file bytes" --> B
V1 — every save re-transfers the entire file, in both directions, regardless of how small the actual edit was.
Why this breaks specifically as files get large or edits get frequent: a one-character edit to a 200-page document, or a single corrected frame in a 4GB video project file, re-uploads and re-downloads the entire object every single time — the sync cost is tied to file size, not edit size, which is exactly backwards for a system whose whole value proposition is "small edits sync quickly." Worse, this design stores and transfers duplicate bytes with no way to notice them: two devices with two near-identical copies of a large file, or two entirely unrelated users who happen to save the same installer or the same shared template, each pay the full storage and bandwidth cost independently, because nothing in this design has any concept of "have we already seen these exact bytes." The fix isn't a faster network or a bigger disk — it's never treating a file as one indivisible unit of transfer or storage in the first place.
K. Data Model & Store Choice
Two structurally different stores for two structurally different access patterns, the same split named generally in Patterns Playbook D and Prerequisites I: a metadata service holds File, FileVersion, Device, and ConflictedCopy records — small, structured, and needing transactional, conditional-write guarantees (the version-vector compare-and-swap at commit time, P, is a textbook instance of Patterns Playbook B's check-then-act race) — while a separate chunk store holds nothing but raw chunk bytes, keyed by content hash rather than an application-assigned ID.
Why the chunk store's key is a content hash, not a random ID — the detail that turns a blob split into deduplication: in every other metadata/blob split in this document (News Feed's media, Chat's attachments, Video Streaming's files, all in Prerequisites I), the blob key is an arbitrary, application-generated identifier — two identical images uploaded by two different users get two different keys and are stored twice. Here, the key is a hash of the content itself, so two identical chunks — whether from two versions of the same file, or two entirely unrelated files from two unrelated users — hash to the same key and are, by construction, the same stored object. Deduplication falls out of the storage layer's addressing scheme itself, not a separate lookup-and-skip step layered on top.
Sharding/routing the chunk store by chunk hash: because chunk hashes are already uniformly distributed, pseudo-random values, they land almost perfectly onto a consistent-hashing ring (Prerequisites J, generalized in Patterns Playbook L) with no further thought required about hot ranges the way Autocomplete's prefix-range sharding needed (P13-P) — an unusually clean case of the pattern.
Why chunk bytes never live inside the metadata database: the same reasoning as Prerequisites I generally — bloating the metadata store's rows with megabytes of binary content would slow every backup, replica, and query touching that store, for the majority of records where nobody actually needs the bytes right now.
L. End-to-End Flow — A User Edits a Large File on Device A; Only the Changed Chunks Sync to the Cloud, Then Down to Device B
The bytes that actually cross the network in either direction are exactly the chunks that changed — everything else, on both devices, is already sitting locally and is never re-transferred.
M. Deep Dive 1 — Chunking Large Files
Requirement this addresses: bandwidth-efficient sync (D) — directly fixing V1's core flaw (J), where sync cost scaled with file size instead of edit size.
Fixed-size chunking, the first thing most candidates propose, and why it mostly fails: slice a file into consecutive N-byte blocks by absolute offset — simple, and it does let an edit near the end of a file re-sync only the chunks after it. But a single byte inserted or deleted anywhere before the end of the file shifts the absolute file offset of every byte after it, which shifts every chunk boundary after that point, which means every single chunk from the edit onward hashes completely differently — even though almost none of their actual content changed. One inserted character near the start of a large file can defeat deduplication for the entire rest of the file. This is the "boundary-shift problem," and it's the specific reason fixed-size chunking, despite sounding like the obvious first move, is not the real answer.
Content-defined chunking with a rolling hash — the fix, and the same core idea rsync's algorithm popularized (AD): instead of cutting at fixed byte offsets, slide a rolling checksum (a Rabin fingerprint or similar) across a small window of bytes, and place a chunk boundary wherever that checksum satisfies a chosen condition (for example, its low N bits are all zero) — a condition that depends only on the local content under the window, never on the byte's absolute position in the file. An insertion or deletion shifts the bytes around it, but every stretch of content before and after the edit still contains the exact same byte sequences it always did, so the rolling hash finds the exact same boundary points around those unchanged stretches — re-producing the identical chunk boundaries, and therefore the identical chunk hashes, for everything the edit didn't actually touch. Only the chunk(s) immediately containing the edit differ.
Chunk size as a tunable trade-off, not a fixed constant: a smaller target chunk size means an edit invalidates fewer, smaller chunks (finer-grained dedup and delta sync, N) but increases the number of chunks — and therefore the size of the manifest and the per-chunk metadata overhead — for every file. A larger target chunk size does the opposite. The chosen average (~4MB, G) is a middle point, and a production system would reasonably vary it by observed file type (smaller chunks for frequently-small-edited documents, larger chunks for media files edited in bulk).
N. Deep Dive 2 — Delta Sync & Chunk-Level Deduplication
Requirement this addresses: the second half of V1's flaw (J) — chunking (M) only produces the right boundaries; delta sync is what actually avoids re-transferring or re-storing content that hasn't changed.
Upload side: after re-chunking locally (M), a device computes the hash of every chunk and calls sync-check (I) to learn which of those hashes the server has never seen before. Only chunks the server doesn't already have get uploaded — a chunk the device already synced in a previous version, or a chunk that happens to be byte-identical to something any other user has ever uploaded, is never re-sent.
Why cross-user dedup, not just per-user version dedup, is the bigger win: because the chunk store's key is a content hash rather than a scoped, per-user identifier (K), a chunk is deduplicated against the entire global chunk store, not just against one user's own file history. Two unrelated users independently saving the same conference-recording file, the same installer, or the same widely-shared template physically store those bytes exactly once, referenced by two completely independent FileVersion manifests. This is the concrete mechanism behind G's dedup-ratio estimate, and it's a strictly larger benefit than per-user dedup alone would ever produce.
Download side, symmetric to upload: a device fetching a newer FileVersion manifest diffs it against its own last-synced manifest (or, for a brand-new device, against whatever it already happens to have cached locally from other files) and downloads only the chunks it's actually missing — reassembling the file locally from a mix of previously-cached and newly-fetched chunks.
Garbage collection, the necessary counterpart to dedup: because one physical chunk can be referenced by many different FileVersion manifests across many files and users, a chunk's bytes can only be safely reclaimed once nothing anywhere references it — tracked via the ref_count field (H), decremented whenever a FileVersion that referenced it is superseded or deleted, with a periodic sweep reclaiming any chunk whose count reaches zero. This is the same deferred-cleanup discipline as reference counting in any shared-object system, applied here to keep the chunk store from accumulating orphaned bytes indefinitely.
O. Deep Dive 3 — Metadata / Blob Store Split, and the Chunk Store as Its Own Layer
Requirement this addresses: storage-efficient, cheaply-scalable data layout (D) — formalizing K's store choice into the concrete pattern this design directly reuses.
This is Patterns Playbook D and Prerequisites I, applied here with one extra layer: the general pattern is "small, frequently-queried metadata in a database; large payload bytes in blob storage," used elsewhere in this document for News Feed's media, Chat's attachments, and Video Streaming's files (all cataloged in Patterns Playbook D). This problem's metadata service plays the identical role — a FileVersion row is small, structured, and holds only a list of chunk hashes, never the bytes themselves. What's different here is that the "blob" side of the split isn't one object per file — it's the chunk store (K, N), itself a single, shared, content-addressed space that every file's manifest draws from. A FileVersion doesn't own its bytes the way a Chat attachment owns its one blob; it merely references a subset of a shared pool, which is exactly what makes cross-file, cross-user dedup (N) possible in the first place — the same metadata/blob split pattern, made more powerful by changing what the blob-store key means.
Why this split matters even more here than in its other appearances: G's capacity table showed metadata's footprint (roughly a petabyte) sitting three orders of magnitude below the chunk store's footprint (exabytes) — an even starker size gap than News Feed or Chat see, because file content genuinely dwarfs a chunk-hash list the way a photo dwarfs a post's caption, but more so. That gap is precisely why the two stores are scaled, replicated, and operated as entirely independent systems: the metadata service can run on fast, transactional, moderately-sized storage while the chunk store runs on cheap, massively-horizontal object storage tuned purely for durable byte throughput.
Why the chunk store still isn't "just" Prerequisites I's blob storage unchanged: ordinary blob storage (S3-style) is addressed by an arbitrary key chosen by whoever's writing; the chunk store here is addressed by a hash of the content itself (K), which is the one modification that turns a generic blob store into a deduplicating one. Worth being explicit about this distinction in an interview — it's a small change to the addressing scheme with an outsized effect on the system's actual storage economics.
P. Deep Dive 4 — Multi-Device Sync & Conflict Detection
Requirement this addresses: detecting a genuine concurrent edit across devices (D) — without assuming any single device or server is continuously online to arbitrate order the moment it happens.
Version vectors, the mechanism: each FileVersion (H) carries a version vector — conceptually a map from device_id to a monotonically increasing counter representing how many times that specific device has committed an edit to this file. When Device A commits a new version, it increments its own entry in the vector it last saw and sends that as based_on_vector (I). The metadata service compares the incoming vector against the file's currently-stored vector: if the incoming vector is a strict descendant of the stored one (every entry is greater than or equal, and at least one is strictly greater, with no entry going backward) it's an ordinary sequential update. If neither vector is a descendant of the other — each has an entry the other lacks — the two edits happened concurrently, and the commit is rejected with a conflict (Q), rather than blindly accepted and overwriting the other side.
Why this is the opposite structural assumption from Google Docs's ordering mechanism, and why that's the correct call here: Google Docs assigns a single, server-issued monotonic revision number per document (P9-O), which works specifically because one always-online coordinating server sits in the loop for every single edit and can assign a strict, global order in real time. That assumption is simply false here — a laptop closed for a business trip, or a phone with no signal for a day, is normal, expected behavior (F), not a rare disconnection Google Docs's design tolerates as an exception (P9-Q). Version vectors are the mechanism built for exactly this: they let two devices that have never been simultaneously online determine, purely by comparing two vectors after the fact, whether their edits were concurrent — no live arbiter required at the moment either edit was made.
Why this detects a conflict without needing to inspect file content at all: the comparison is pure metadata — two small vectors — so detecting "these two edits collided" costs nothing proportional to file size, and happens at commit time, before any large chunk transfer is even attempted for the losing side.
Q. Deep Dive 5 — Conflict Resolution: Why a Conflicted Copy, Not an Automatic Merge
Requirement this addresses: the no-silent-data-loss NFR (D) — once P detects a genuine conflict, something has to happen to both sides that isn't "discard one."
The direct contrast with Google Docs (P9), stated explicitly: Google Docs resolves a conflict by merging it — operational transformation (P9-N) mathematically combines two concurrent text operations into one coherent document, because an "insert character X at position 10" operation has well-defined, composable semantics: given two such operations, there is a mechanical, correct way to transform one against the other and produce a single result nobody needs to review. A changed chunk carries no equivalent semantics. "Chunk 7's hash changed" says nothing about what changed inside it — whether a paragraph was added, a row was deleted from a spreadsheet, or a few bytes of a binary header were altered — and the sync layer, by design (F), never inspects file content closely enough to know. There is no general transformation function that takes two divergent versions of an arbitrary chunk and produces one correct merged chunk, because "correct" depends on semantics the sync layer deliberately doesn't have access to. Attempting to guess — interleaving bytes, or picking one side's chunk over the other — doesn't reconcile the conflict, it silently risks corrupting the file, which is a strictly worse outcome than admitting the conflict exists.
The fix — keep both, and let the human decide: whichever commit the metadata service accepts first (P) keeps the original file_id and path, and syncs out normally to every other device. The commit that lost the version-vector race is not discarded — it's materialized as a new ConflictedCopy record (H), written out at a derived path ("report (Jamie's conflicted copy, 2024-06-01).xlsx"), and synced down to every device just like any other new file. Nothing is lost; nothing is auto-merged; the two divergent versions simply become two separate, fully-preserved files, and reconciling the actual overlap becomes a manual decision made by a person looking at both, not a decision the system makes silently on their behalf.
Why this isn't a workaround or a lesser design than Google Docs's — it's the correct answer to a genuinely different data model: if the underlying content were guaranteed composable (structured text edits with known semantics), automatic merging would be strictly better and this design would be missing something by not doing it. But because file content here is arbitrary and opaque by design assumption (F), conflicted-copy resolution isn't a simplification of Google Docs's approach — it's the industry-wide answer every general-purpose file-sync product converges on for exactly this reason, independent of vendor.
R. Deep Dive 6 — Offline Edits Queued Locally, Synced on Reconnect
Requirement this addresses: the offline-tolerant NFR (D) — a device with no connectivity must keep working locally and catch up correctly later, treated as the normal case (F), not an exception.
Local-first editing, and a durable local write-ahead queue: a device re-chunks (M) and computes new manifests on every save regardless of network state, and continues serving the file to the user entirely from its own local chunk cache the whole time it's offline — there is no user-visible difference between "editing while connected" and "editing while disconnected" from the local client's point of view. Every pending chunk upload and the eventual commit intent are appended to a small, durable local queue (a write-ahead log on local disk, so an app crash or device reboot doesn't lose queued work), each entry tagged with a stable, client-generated operation ID — the same idempotency discipline named generally in Prerequisites O — so that a queue flush interrupted partway through and retried from the start never double-uploads a chunk or double-commits a version.
On reconnect: the client first flushes any queued chunk uploads (N), then replays its queued commit exactly as it would have sent it in real time, still carrying whatever vector it was originally based on (P). If that vector is still the file's current stored vector — nobody else edited the file in the meantime — the commit lands cleanly, no differently from an edit that was never delayed at all. If the stored vector has since moved forward because another device committed while this one was offline, the commit is rejected as concurrent, and resolved through Q's conflicted-copy path exactly as it would be for two devices editing milliseconds apart while both online.
Why offline editing isn't a separate mechanism, only a longer delay before the existing one runs: this deliberately mirrors how Google Docs's own offline deep dive (P9-Q) reuses its online conflict machinery for a longer gap rather than inventing a new algorithm — the parallel stops there, though: Docs replays queued operations through OT and gets one merged document regardless of how long the gap was; this design replays queued commits through version-vector comparison and gets either a clean update or a conflicted copy, precisely because of Q's contrast between the two data models. A long offline period here doesn't change the mechanism, just the odds that the vector has moved on by the time reconnection happens.
S. Deep Dive 7 — Notifying Other Devices of Changes
Requirement this addresses: timely propagation (D) of "something changed, go sync" to a user's other devices — without paying for a resource commitment that doesn't match how those devices actually spend most of their time.
| Approach | How | Trade-off |
|---|---|---|
| Periodic poll | Each device periodically asks the metadata service "has anything changed since my last check?" | Simplest to build and cheapest per idle connection, but adds up to a full poll interval's worth of propagation latency, and wastes a request on every poll that finds nothing new — at hundreds of millions of mostly-idle devices, that waste is real |
| Persistent WebSocket (Prerequisites K) (chosen default for actively-open clients) | A true server-push channel, the same connection-management approach Chat System and Google Docs both reuse for their own real-time paths | Lowest possible latency, but requires holding a live connection per device — a materially heavier resource commitment here than in Chat or Docs, where the underlying assumption is a session actively being used right now; a large fraction of this system's registered devices are laptops that are closed or phones that are backgrounded for most of a day |
| Long-polling (chosen default for the common case) | A device holds one HTTP request open against a "listen for changes" endpoint; the server holds that request open and only responds once something changes (or a timeout elapses), at which point the device immediately reopens another one | A genuine middle ground — near-push latency without committing to a permanently-held, always-provisioned connection the way a raw WebSocket does, and it's the actual mechanism Dropbox's own public API uses for this exact purpose (AD) |
The chosen default, and why: long-polling as the primary channel for the ordinary "client running in the background, not being watched by a human waiting on a keystroke" case (D's much looser latency budget than Docs or Autocomplete, established in G) — with an optional upgrade to a platform-native push mechanism (mobile OS push notification services) for devices that are backgrounded or fully closed, rather than the client itself holding any connection open at all in that state. A persistent WebSocket is kept available for a client that's actively open and being watched, but isn't assumed as the default for every registered device simultaneously, unlike Chat System's connection fleet where a live connection is the normal state for an online user.
T. Full Solution at Scale — Final Architecture
(local chunker + offline WAL, M, R)"] --> API["Sync API / Gateway"] DevB["Device B Client"] -.->|"long-poll / push, S"| NotifySvc API --> Meta["Metadata Service
(files, versions, version vectors, P)"] Meta --> MetaDB[("Metadata DB
File, FileVersion manifests, O")] Meta --> ConflictSvc["Conflict Detector
(version vector comparison, P)"] ConflictSvc -->|"concurrent edit detected"| ConflictHandler["Conflicted Copy Handler
(Q)"] ConflictHandler --> MetaDB API --> ChunkRouter["Chunk Store Router
(consistent hash by chunk hash, K, N)"] ChunkRouter --> ChunkShard1[("Chunk Store Shard 1
content-addressable, O")] ChunkRouter --> ChunkShard2[("Chunk Store Shard 2")] ChunkRouter --> ChunkShard3[("Chunk Store Shard 3")] Meta --> NotifySvc["Change Notification Service
(long-poll / push, S)"] NotifySvc -.-> DevB subgraph GC [" Background GC (N) "] direction LR RefCount["ref_count sweep"] --> Reclaim["Reclaim orphaned chunks"] end ChunkShard1 --> RefCount ChunkShard2 --> RefCount ChunkShard3 --> RefCount
Everything above is the composition of M through S — chunking and delta sync keep byte transfer proportional to the edit, the metadata/chunk split keeps the two stores scaling independently, and version vectors plus conflicted copies handle the case where two devices genuinely collide.
U. Design Evolution
| Version | What Changed | Why |
|---|---|---|
| V1 | Whole-file upload/overwrite and whole-file download on every change, no dedup at all (J) | Simplest possible thing that satisfies the functional requirement in isolation |
| V2 | Fixed-size chunking replaces whole-file transfer | A first attempt at partial transfer — but the boundary-shift problem defeats dedup for most real edits (M) |
| V3 | Content-defined, rolling-hash chunking replaces fixed-size slicing | Chunk boundaries survive insertions and deletions, so an edit invalidates only the chunks it actually touches (M) |
| V4 | A global, content-addressed chunk store enables upload/download-side delta sync and cross-user dedup | Bandwidth and storage cost now scale with the edit, and with how much content is genuinely unique platform-wide, not with file size or user count (N) |
| V5 | Metadata service and chunk store formalized as two independently-scaled systems | A three-order-of-magnitude size gap between metadata and chunk bytes (G) makes shared scaling wasteful for both (O) |
| V6 | Per-file version vectors added to detect concurrent multi-device edits | Detects genuine conflicts without requiring a single, continuously-online ordering authority devices can't realistically always reach (P) |
| V7 | Conflicted-copy resolution replaces any notion of silent overwrite or automatic merge | Arbitrary file content has no composable merge semantics — surfacing the conflict is the only safe move, unlike Google Docs's mergeable text operations (Q) |
| V8 | A durable local write-ahead queue lets a device keep editing fully offline | Extends the same commit/conflict machinery to an arbitrarily long gap before reconnecting, without inventing a separate offline algorithm (R) |
| V9 | Long-polling (with optional platform push) notifies other devices that a sync is needed | Propagates changes promptly without assuming every registered device holds a permanently live connection (S) |
What stays stable throughout: the small, chunk-hash-oriented sync-check/commit contract from I — every later refinement changes how chunks are produced, deduplicated, or reconciled, never the basic shape of what a device actually sends and receives.
V. Consistency Model
This system is eventually consistent and explicitly offline-tolerant, with conflicts surfaced rather than silently merged or silently discarded — a third distinct consistency shape worth naming precisely, alongside the two most common shapes named elsewhere in this document. It is not Ticketmaster's narrow strong-consistency island (Patterns Playbook J): there is no single scarce resource being contended for here — no seat, no leader lock — so there's nothing to reject outright the way an oversold seat purchase must be. It is also not Google Docs's strong eventual consistency (P9-V), where any two replicas that have seen the same operations are guaranteed to converge automatically to one identical state, regardless of arrival order. This design's version-vector compare-and-swap at commit time (P) is, mechanically, the same check-then-act race named in Patterns Playbook B — but where every other instance of that pattern in this document resolves a "losing" request by rejecting or blocking it, here the losing commit isn't rejected outright, it's forked into a conflicted copy (Q). Two replicas that genuinely raced do not silently converge to one shared state the way Docs's replicas do — they deliberately land on two distinct, both-preserved files, and a human closes the gap OT/CRDTs would have closed automatically. Every other consequence of eventual consistency here — a device that hasn't synced in days simply catching up once it reconnects (R), a notification arriving some seconds late (S) — costs nothing beyond delay; the one genuinely different case, a real concurrent edit, costs a visible fork rather than a silent loss, which is the entire point of choosing this shape over either of the other two.
W. Failure Scenarios
- The metadata service is unavailable during a commit attempt: the device's edit is already sitting safely in its local offline queue (R) regardless of server availability — the commit simply retries once the service recovers, with no data at risk in the meantime, the same graceful degradation offline editing already has to handle as its normal case.
- A chunk store shard becomes unavailable: devices whose manifests reference chunks on that shard see a partially-synced file (missing just those chunks) rather than a failed sync entirely — chunk bytes are replicated across multiple storage nodes/AZs as baseline durability, so a single shard's temporary unavailability, not permanent data loss, is the expected failure mode; sync completes once the shard (or its replica) recovers.
- Two devices commit concurrently to the same file: this is the case the entire version-vector/conflicted-copy design exists for (P, Q) — not really a failure so much as the expected, correctly-handled outcome; both edits survive, as two separate files, and nothing is silently lost.
- A device crashes mid-chunk-upload: a partially-uploaded chunk is never referenced by any committed
FileVersionmanifest, so it's simply inert, orphaned data cleaned up by the ref-count GC sweep (N) — on restart, the device's durable local queue (R) resumes the upload from wherever it left off, using its stable operation ID to avoid any duplicate effect. - A chunk is unrecoverable due to a rare storage failure, despite replication: the specific
FileVersionreferencing that chunk is flagged corrupted, and the affected device falls back to the most recent version whose full chunk set is verified present, rather than silently serving a file with a missing piece. - The change-notification service is down: other devices simply don't get a timely "go sync" nudge — no state is lost, discovery of "something changed" just degrades to whatever periodic fallback interval a device falls back to once its long-poll connection fails, the same soft-degradation shape as Autocomplete's trending-overlay outage (P13-W).
X. Bottlenecks & Scaling Boundary
| Stage | Actual Bottleneck | Fix Applied |
|---|---|---|
| V1 whole-file transfer | Bandwidth and storage cost scale with file size, not with how much actually changed | Content-defined chunking + delta sync (M, N) |
| Fixed-size chunking | An insertion or deletion shifts every subsequent chunk boundary, defeating dedup for most of the file | Rolling-hash, content-defined chunk boundaries (M) |
| Per-user-isolated storage | Identical bytes across unrelated users' files stored redundantly, with no way to notice the duplication | A single, global, content-addressed chunk store (K, N) |
| Metadata and chunk bytes sharing one store | A three-order-of-magnitude size mismatch (G) forces the smaller, transactional metadata workload to scale alongside the much larger byte workload | Metadata/chunk store split, scaled and operated independently (O) |
| Assuming a single always-online ordering authority | Devices are routinely offline for hours or days (F) — a live arbiter can't assume continuous connectivity from every participant | Version vectors compared after the fact, not a live-assigned sequence (P) |
| Attempting automatic merge of arbitrary content | No general, safe way to combine two divergent versions of opaque bytes without risking silent corruption | Conflicted-copy resolution (Q) |
| Every registered device holding a permanent connection | Resource cost for hundreds of millions of devices that are closed or backgrounded for most of a day | Long-polling as the default channel, with platform push for backgrounded devices (S) |
Y. Design Trade-Offs
| Decision | Option A | Option B | Trade-off |
|---|---|---|---|
| Chunking strategy | Fixed-size, offset-based | Content-defined, rolling-hash (chosen) | Fixed-size is simpler to implement but any insertion/deletion defeats dedup for everything after it; content-defined survives edits at the cost of a variable, slightly less predictable chunk size distribution |
| Deduplication scope | Per-user only | Global, cross-user, content-addressed (chosen) | Per-user dedup is simpler and has no cross-tenant data-boundary questions to reason about; global dedup captures a materially larger real-world storage win (G) at the cost of a shared chunk-store keyspace design |
| Conflict resolution | Automatic merge (OT/CRDT-style) | Conflicted copy, human-resolved (chosen) | Automatic merge would be strictly better if content were reliably composable, but arbitrary file bytes aren't (Q) — attempting it risks silent corruption; a conflicted copy is less elegant but never wrong |
| Conflict detection authority | Single always-online coordinating server (Google Docs's model) | Version vectors, compared after the fact (chosen) | A live authority gives a strict, real-time order but assumes continuous connectivity this system's devices don't have (F); version vectors work correctly across arbitrarily long offline gaps, at the cost of only detecting concurrency in hindsight rather than preventing it live |
| Change notification channel | Persistent WebSocket for every device | Long-polling by default, push for backgrounded devices (chosen) | A WebSocket gives the lowest latency but assumes a live connection is the normal state, which doesn't match how most registered devices actually spend their time; long-polling/push matches the real usage pattern at a small latency cost |
Z. Common Bad Designs
- Uploading and downloading the entire file on every change. Ties bandwidth and storage cost to file size instead of edit size — the single most important mistake to avoid on this problem, and the one that looks fine in a small-file demo.
- Fixed-size chunking, assumed sufficient for dedup. Looks like a real fix until a single insertion or deletion is tested, at which point it defeats dedup for the rest of the file.
- Scoping deduplication to just one user's own files. Misses the larger, genuinely available win of global cross-user dedup, for no real benefit in exchange.
- Reaching for operational transformation or a CRDT to "merge" conflicting file edits, because it worked for Google Docs. Arbitrary file bytes have no composable edit semantics — this is the single most common way a candidate who's studied Google Docs (P9) gets this specific problem wrong.
- "Last commit wins, silently discard the other." The same silent-data-loss failure mode as Google Docs's own V1 (P9-J) — a legitimate edit vanishes with no warning, which is exactly what this design's no-silent-data-loss requirement (D) exists to prevent.
- Assuming a single, continuously-online authority can sequence every device's edits in real time. A structurally false assumption once offline editing (F) is taken seriously — the entire reason version vectors (P), not a live sequence number, are the mechanism here.
AA. Interviewer Follow-Ups
| Follow-Up | Design Change | Components Changed | Components Unchanged |
|---|---|---|---|
| "What about very large files — multi-gigabyte video project files?" | Parallelize chunk upload/download across multiple concurrent transfers, and make chunk size itself tunable per file type so a huge file's manifest doesn't explode in chunk count | Transfer layer, chunk-size tuning in M | Metadata model, version vectors, conflict resolution — none of it is file-size-dependent |
| "How do shared folders across multiple users work?" | The version vector's key generalizes directly from device_id to (user_id, device_id), and a conflicted copy's derived filename simply includes the other user's name instead of another of the same user's device names | Vector key shape, conflicted-copy naming (P, Q) | The chunking, delta sync, and chunk store mechanics — entirely unaffected by whether the two concurrent editors share an account |
| "What if we actually wanted real-time collaborative editing on a specific file type, like a shared spreadsheet?" | That's a different, additional problem — Google Docs's OT/CRDT model (P9) would be layered on top of a specific application understanding that file's structure, not solved by this file-sync layer, which stays deliberately content-agnostic (F) | A new, app-specific collaboration engine sits above this system | The chunked file-sync engine underneath stays exactly as designed — it becomes the storage substrate that engine's snapshots ultimately persist to |
| "How do you keep a client from uploading or downloading someone else's chunks?" | Access control is checked at the metadata layer before a chunk hash or a pre-signed URL is ever handed to a client (Prerequisites I's pre-signed URL pattern) — the chunk store itself stays a dumb, permission-agnostic byte store keyed only by content hash | A new authorization check in front of chunk address resolution | The chunk store's internals, its addressing scheme, and its dedup properties |
AB. Interviewer Evaluation Rubric
- Weak candidate: proposes re-uploading/re-downloading the whole file on every change, with no concept of chunking or deduplication, and has no answer for why a small edit to a large file should be treated differently from uploading it fresh.
- Mid-level: reaches for chunking correctly but proposes fixed-size chunks without noticing the boundary-shift problem, and treats "detect a conflict" and "resolve a conflict" as the same step rather than two genuinely distinct mechanisms.
- Senior: proactively reaches for content-defined chunking and explains why it survives insertions/deletions where fixed-size chunking doesn't; separates the metadata service from the chunk store cleanly; reasons concretely about version vectors as a way to detect concurrency without a live arbiter.
- Staff+: is unprompted and explicit about why this problem can't reuse Google Docs's OT/CRDT merge — tying the reasoning to composability of the underlying edit model, not just naming both mechanisms — and connects the metadata/chunk split explicitly to the same pattern used elsewhere in the document while correctly identifying what's genuinely different about it here (content-addressed keys enabling dedup, not just a size split).
AC. Interview Simulation
AD. Online Solution Comparison
| Source | Approach | Strength | Weakness | Worth Learning |
|---|---|---|---|---|
| Dropbox Tech Blog — "Rewriting the heart of our sync engine" (official engineering blog) | Describes Dropbox's real production sync engine (Nucleus), built around block-level content addressing so that only the changed portions of a file are ever transferred or re-stored, plus the practical difficulty of correctly detecting and reconciling changes made across many devices and long offline periods | A direct, production confirmation that block/chunk-level content addressing and delta sync, not whole-file transfer, is the real mechanism behind Dropbox's actual sync behavior — and that offline/multi-device reconciliation is treated as a first-class, genuinely hard problem in production, not an interview simplification | The post focuses on engine architecture and migration rationale rather than publishing the exact chunking algorithm or conflict-resolution rules in full detail | Confirms that "chunk, dedupe, sync only the delta" is the real production answer at Dropbox's own scale, not a textbook simplification of what they actually do |
| "The rsync algorithm" — Andrew Tridgell & Paul Mackerras (original technical report, hosted on rsync.samba.org) | Describes rsync's rolling-checksum-based algorithm for efficiently transferring only the differing portions of two versions of a file over a slow link, without both sides needing to already have a shared prior version in the same place | The original, foundational description of exactly the rolling-hash, content-boundary-finding idea this design's chunking mechanism (M) is built on — a legitimate, decades-old, still-relevant algorithm, not an invented interview shortcut | rsync's specific algorithm compares two known endpoints directly rather than maintaining a persistent, globally shared, content-addressed store the way a multi-device sync product needs to — the core rolling-checksum idea transfers directly, but the surrounding system design doesn't | Confirms that rolling-checksum, content-defined boundary detection is a well-established, rigorously described technique — worth knowing its origin, not just its application here |
| Pro Git Book — "Git Internals: Git Objects" (official Git documentation) | Describes how Git stores every blob (file content) addressed by the SHA-1/SHA-256 hash of its own bytes — two files with identical content are, by construction, stored as the exact same object, and later commits reference deltas against these content-addressed objects rather than storing full copies each time | A different, extremely widely-used real system built on the identical core idea this design's chunk store relies on: content-addressed storage as the mechanism that makes deduplication automatic rather than a separately-implemented lookup step | Git's object model is optimized for a version-controlled repository's access patterns (diffing commits, history traversal), not for large binary media files or continuous multi-device background sync the way this problem requires | A clean, independent confirmation that "address storage by a hash of its own content" is a proven, general pattern for automatic deduplication — useful to cite as a second, very different domain reaching the same underlying design decision |
AE. Interview Cheat Sheet
| Problem | Distributed File Storage / Sync (Dropbox) |
| Difficulty | Advanced — individually simple mechanisms, hard to compose correctly, and easy to mistakenly borrow Google Docs's merge model where it doesn't apply |
| Core requirements | Multi-device file sync, bandwidth/storage efficient, offline-tolerant by default, no silent data loss on conflict |
| Central decision | Content-defined chunking + a global content-addressed chunk store for delta sync and dedup, a metadata/chunk store split, version vectors for conflict detection, conflicted copies (not merges) for conflict resolution |
| Why not whole-file transfer | Sync cost scales with file size instead of edit size — exactly backwards for a system whose value proposition is syncing small edits quickly |
| Chunking | Content-defined via a rolling hash, not fixed-size — survives insertions/deletions where fixed-size chunking's boundary-shift problem destroys dedup |
| Dedup scope | Global and cross-user, via content-hash addressing — not scoped per user |
| Conflict handling | Version vectors detect concurrency without a live arbiter; conflicted copies resolve it — deliberately not an automatic merge, because arbitrary file bytes aren't composable the way Google Docs's text operations are (P9) |
| Consistency | Eventually consistent and offline-tolerant, with conflicts explicitly surfaced as a fork rather than silently merged (Docs's model) or silently rejected (Ticketmaster's model) |
| Common mistakes | Whole-file re-transfer; fixed-size chunking; per-user-only dedup; reaching for OT/CRDT merge logic; silent last-write-wins; assuming a single always-online ordering authority |
| What NOT to over-engineer | A general-purpose semantic merge engine for arbitrary file types — name the conflicted-copy approach as the deliberate, correct answer for opaque content (Q, Z), not a limitation to apologize for |