03. File Sync — Interview Q&A¶
~15 min read · Part 4 of 4 (Overview → HLD → LLD → Q&A)
These are the questions an interviewer actually asks once the two rails are on the board. Each answer is the strong version, followed by the wrong answer that quietly sinks candidates.
Q1. How do you avoid re-uploading a whole file when only a small part changes? Split every file into fixed 4 MB blocks, hash each block, and represent the file as an ordered list of block hashes. A change re-hashes the file and diffs the new hash list against the old, so only the blocks whose content actually changed are uploaded. For the running example, a one-line edit to a 1 GB video rewrites exactly one of its 256 blocks, so 4 MB crosses the wire instead of 1,024 MB — a 256:1 reduction. The reframing to say out loud is that syncing a large edit is a diffing problem, not a bandwidth problem. Common wrong answer to avoid: "Compress the file and upload it." Compression shrinks the whole file every time; it still re-sends a gigabyte to move a kilobyte of real change. The win comes from not sending unchanged blocks at all.
Q2. Why content-address the blocks, and what does that buy you? Key each block by the hash of its own content. Two identical blocks — whether inside one file, across two files, or uploaded by two different users — collapse to the same key and are stored exactly once, so deduplication falls out for free: the second uploader's probe reports zero missing blocks and transfers nothing. A 1 GB onboarding video synced by 1,000 employees is stored once, not 1,000 times — a 1,000:1 storage reclaim on that asset, on top of the ~30% fleet-wide dedup that content addressing gives across ordinary duplicated content. Common wrong answer to avoid: "Key blocks by file ID and offset." Then identical content under two files gets two keys, you store it twice, and cross-file and cross-user dedup is impossible.
Q3. Two devices edit the same file at the same instant. What happens?
Both devices computed their change against version N and both try to commit as N+1, each naming parent_cursor = N. The metadata service serializes commits through a single per-namespace cursor: the first to arrive matches the head and wins, advancing it to N+1; the second arrives with a now-stale parent and is rejected with a conflict. Rather than discard the loser's edit, the service materializes it as a conflicted copy — demo (Device-B's conflicted copy).mp4 — so both edits survive and every device converges on the same two files. It is deterministic because the single serialization point orders the commits identically for everyone, and the conflicted-copy name is a pure function of the losing device's ID.
Common wrong answer to avoid: "Last write wins, keep the newer timestamp." Client clocks disagree and skew, so timestamp ordering is neither deterministic nor safe, and it silently destroys the losing edit — which for opaque files you can never recover.
Q4. Why not just merge the two concurrent edits automatically? Because for an opaque file — a video, a zip, a compiled binary — there is no semantic merge; interleaving two byte streams produces garbage, not a merged file. Content merging only works when the format is understood and mergeable, which is what a collaborative document editor does with operational transforms or CRDTs, and that is a different system. For general files the honest guarantee is "one deterministic winner keeps the name, the loser is preserved beside it, nothing is lost," and preserving both is strictly better than a merge that corrupts. Common wrong answer to avoid: "Use a CRDT so edits always merge." CRDTs merge structured state; they cannot meaningfully merge two independent edits to an opaque 1 GB video, and reaching for one here signals not understanding what is mergeable.
Q5. Why split metadata and blocks into two stores with different consistency? Because they have opposite needs. The file tree and commit order must be strongly consistent — every device replays the same ordered log to reach byte-identical state — so metadata lives in a sharded transactional store with a single serialization point per namespace. Blocks are immutable and content-addressed; a block either exists under its hash or it does not, with no ordering to get wrong, so the blob store can be eventually consistent, geo-replicated, and erasure-coded for cheap durability. Splitting along that seam keeps the expensive-to-coordinate part small (metadata) and lets the enormous part (140 PB of blocks) need no coordination at all. Common wrong answer to avoid: "Store everything in one strongly consistent database." You would pay coordination cost on 140 PB of immutable blocks that never needed it, and the blob volume would swamp the transactional store's throughput.
Q6. How do you shard the metadata, and what is the hard case?
Shard by namespace_id, where a namespace is one user's root or one shared folder. Every commit for a folder then hits one shard, so advancing the cursor is a single-row conditional update with no distributed lock, and adding shards adds commit throughput linearly across the ~23,000 commits/second peak. The hard case is a shared folder with many concurrent editors, which concentrates commits and optimistic-concurrency retries on one shard; you serialize that namespace's commits through a short server-side queue so writers wait briefly instead of thrashing the retry loop.
Common wrong answer to avoid: "Shard by file ID" or "shard by block hash for everything." Sharding metadata by file scatters a single folder's operations across shards and destroys the single serialization point that makes ordering cheap.
Q7. If dedup means many files share a block, how do you ever delete anything? Reference-count each block: increment when a version referencing it commits, decrement when a version is finally purged past retention. A block is eligible for deletion only when its refcount reaches zero — no file anywhere points at it. Even then you do not delete immediately; you tombstone with a grace period, because a commit in flight may be about to reference that exact content again. Deleting the instant the count hits zero is the classic dedup GC race that loses a block out from under a concurrent upload. Common wrong answer to avoid: "Delete the block when the file that uploaded it is deleted." That corrupts every other file that deduplicated against it — the whole point of content addressing is that the block no longer belongs to one file.
Q8. Walk through the storage and bandwidth math. At 100M registered users and ~2 GB each, that is ~200 PB logical; block-level dedup reclaims ~30% in the common case, so ~140 PB physical, which at 4 MB blocks is ~50 billion content-addressed objects. Writes are ~2,300 metadata commits/second average, ~23,000 at peak, spread across namespace shards. Upload bandwidth is ~5.8 GB/second average and ~58 GB/second peak because of delta sync — the naive whole-file alternative would multiply that by the ratio between file size and change size, which for the scenario's video is 256×. The math is the justification for block sync, not decoration. Common wrong answer to avoid: "We'll need an exabyte-scale database for the metadata." The metadata is small and relational; the exabytes are immutable blocks in an object store. Conflating the two picks the wrong tool for both.
Q9. How does a device learn that something changed without hammering the server? Each device holds one long-lived connection to a notification service — ~10 million connections across ~200 nodes — that pushes a lightweight "namespace advanced to cursor C" nudge when a commit lands. The device then pulls the authoritative delta since its last cursor. The notification is only a hint carrying no file data, so a lost or dropped notification costs latency (the device falls back to periodic polling) but never correctness, because the cursor in the metadata store is the source of truth. Common wrong answer to avoid: "Every client polls every few seconds for changes." Ten million clients polling is a self-inflicted load spike, and shortening the interval to cut latency multiplies it; long-poll/push with a poll fallback is what scales.
Q10. A metadata shard goes down. What breaks, and what keeps working? Commits to namespaces on that shard stall and clients queue local edits until it recovers; reads can be served from a replica. Nothing on the block rail is affected — already-synced files stay fully readable because their blocks live in the geo-replicated object store. This is exactly why the shard runs with synchronous replicas and fast failover: a metadata pause is painful but survivable, while a metadata loss would break the guarantee that an acknowledged commit is durable, which is unacceptable. Common wrong answer to avoid: "The whole service goes down." Only commits to the affected namespaces pause; the two-rail split means blocks and other shards are unaffected, and conflating the two overstates the blast radius.
Q11. Fixed-size blocks or content-defined chunking? Fixed 4 MB blocks are the default: cheap to compute, and they give clean positional diffs for in-place edits — the scenario's edit that doesn't change file length re-uploads exactly one block. Their weakness is insertion: adding bytes in the middle shifts every following boundary, so a naive fixed scheme re-uploads the whole tail. Content-defined chunking anchors boundaries to content via a rolling hash, so an insertion disturbs only the nearby chunk, at the cost of meaningfully more CPU per byte and variable block sizes. Choose fixed blocks for edit-in-place and cross-file dedup, and reach for CDC when the workload is dominated by insertions. Common wrong answer to avoid: "Always use content-defined chunking, it's strictly better." It is not free — the rolling-hash CPU cost is real, and for in-place edits and whole-block dedup fixed blocks match it while being simpler and faster.
Q12. How do you make commits and uploads safe to retry after a network timeout?
Both are idempotent by construction. A block PUT is keyed by content hash, so a retry re-writes identical bytes under the same key — a no-op, never a duplicate. A commit names its parent_cursor; if the first attempt actually landed before the timeout, the head already advanced, so the retry gets a conflict and the client reconciles via delta instead of committing twice. Attaching a commit UUID the service can dedup collapses the retry even more cleanly.
Common wrong answer to avoid: "Retry the commit with the latest cursor." Blindly re-parenting to the new head would apply your change on top of whatever you collided with, silently overwriting it — the retry must go through the conflict path, not around it.
Deeper follow-ups¶
- How would you support sharing a folder between two users whose roots are on different metadata shards, given that a shared folder needs one serialization point?
- How would you enforce a per-user storage quota when dedup means a block's bytes are shared across many users — whom do you bill for a shared block?
- How would you add client-side end-to-end encryption while preserving cross-user dedup, given that encrypting a block with a per-user key destroys the shared hash?
- How would you keep a device that has been offline for a month from stampeding the server with a huge delta and thousands of block fetches on reconnect?
- How would you bound version-history storage — how long do you keep old versions and orphaned blocks before GC, and how does that interact with refcounting?
- How would you detect and throttle a client whose folder-watcher is thrashing (a build directory rewriting thousands of files per minute)?
How this round is scored¶
Interviewers use file sync to see whether you separate the immutable, dedup-friendly block problem from the ordered, consistency-critical metadata problem — candidates who keep them fused give away that they have not thought about where coordination actually costs. The strong signal is arriving early at "a file is an ordered list of content-addressed block hashes," because every good property (delta sync, dedup, cheap moves, free versioning) falls out of that one representation. Seniority shows in the tradeoff discussions — dedup complexity versus storage saving, strong metadata versus eventual blobs, and above all the conflict strategy, where the mature answer is deterministic-winner-plus-preserved-loser rather than a lossy last-writer-wins or a fantasy of merging opaque files. The concurrency section (the two-device edit, GC-versus-commit races, idempotent retries) separates people who have operated sync systems from people who have only drawn them, and doing the block math out loud — 256:1 on the edit, 1,000:1 on the shared video, 200 PB down to 140 PB — is what turns a correct answer into a senior one.