IntervueClubbeta
Continue where you left off?

Reading long content?

Light mode reduces eye strain for sustained reading sessions.

🧠 You've been reading for 20 minutes

A short break now helps your brain consolidate what you just read. You'll retain more.

🌙 Reading late?

Your brain absorbs less after midnight. Consider bookmarking this and coming back fresh — you'll get more out of it.

/ to search·TOC on the right to jump sections·dark/light toggle in nav

Design Dropbox

hardStoragefile-storagesyncchunkingdeduplicationconsistencys3
Jul 2, 2026·~21 min read
Asked atDropboxGoogleMicrosoftAppleBox
Watch Video Walkthrough
Watch the author walk through the problem step-by-step

Problem Statement#

"Design a file sync service like Dropbox. Users should be able to store files in the cloud and have them sync automatically across all their devices."

That's it. One sentence. The rest is yours to figure out.


Requirements Gathering#

This is where the interview actually begins. A strong candidate doesn't start drawing boxes. They ask questions. Not to stall, but because the answers fundamentally change the design.

Here's how a good requirements conversation looks:


Candidate: "Are we designing cross-device sync, or real-time collaborative editing like Google Docs? Those are very different problems."

Interviewer: "Cross-device sync. A file edited on one device shows up on the others."


Candidate: "What's the max file size? That decides whether we can treat a file as one blob or must chunk it."

Interviewer: "Files up to 10 GB. Design for large files."


Candidate: "Do we need versioning and conflict resolution when the same file is edited on two devices?"

Interviewer: "Yes. Keep version history, and handle concurrent edits sanely."


Candidate: "Mobile clients too, or desktop only? That drives how aggressive we get about bandwidth."

Interviewer: "Both. Bandwidth and battery matter on mobile."


Candidate: "Do users share files and folders with other people?"

Interviewer: "Yes, sharing with specific people, including folders."


Candidate: "What scale? Users and total storage?"

Interviewer: "700 million registered users, ~1.4 exabytes of data."


Candidate: "Out of scope: real-time co-editing, thumbnails and media transcoding, enterprise admin and compliance?"

Interviewer: "Correct, out of scope. Just sync, versioning, and sharing."


Functional and Non-Functional Requirements#

Functional Requirements#

  1. Users upload files from any device; files are stored durably in the cloud
  2. Files sync automatically to all of a user's connected devices
  3. Users share files and folders with specific people
  4. File versioning: restore previous versions (last 30 days)
  5. Conflict resolution when a file is edited on two devices at once
  6. Offline support: offline changes sync when connectivity returns

Non-Functional Requirements#

RequirementTarget
Metadata consistencyStrong (file tree, versions)
Blob consistencyEventual (briefly stale read acceptable)
BandwidthTransfer only changed bytes
Durability11 nines on blob storage (S3-class)
Scale700M users, files up to 10 GB, ~1.4 EB total

Out of scope: Real-time collaborative editing (Google Docs OT/CRDT), thumbnails and media transcoding, enterprise admin/compliance controls

Capacity Estimates#

Work through the math. Don't just state the numbers.

code
Upload QPS (baseline):
  If 1% of 100M DAU upload one file/day = 1M uploads/day = ~12 uploads/sec
  Peak is bursty (workday hours) → design for ~1,000 uploads/sec

Chunk records (4 MB chunks, ~10 chunks/file at peak):
  1,000 uploads/sec × 10 = ~10,000 chunk manifest writes/sec

Sync polling:
  100M DAU checking for changes = ~1,200 change-checks/sec average

Blob storage:
  1.4 EB → object storage (S3 or equivalent), content-addressed

Dedup savings (industry benchmark):
  20-40% of chunks are duplicates → large storage + bandwidth reduction "for free"

The insight that reframes the whole problem: storing the bytes is solved: S3 does it. The number that matters is bandwidth. Re-uploading a 10 GB file because one paragraph changed is the failure mode, and chunking plus delta sync is what avoids it.


High-Level Design#

Two flows drive everything: a file changes locally and must go up, and a change elsewhere must come down to every other device. The sync client is the brains; the server is mostly bookkeeping and storage.

code
[Desktop / Mobile Client]  ──(sync daemon watches filesystem)
        │  chunk + hash locally

[Sync Service] ──────────────► [Metadata DB: Postgres]  (file tree, versions, manifests)
        │  dedup check                   │
        ▼                                ▼
[Block Store: S3]  (chunks by content hash)   [Notification Service → other devices]

Write path (up): The client watches the filesystem, splits changed files into chunks, hashes each chunk, and asks the Sync Service which hashes are new. It uploads only the new chunks (via pre-signed S3 URLs), then commits a new version with a chunk manifest to the Metadata DB, which fires a change notification.

Read path (down): A notified device fetches the new manifest, diffs it against its local manifest, downloads only the chunks it's missing, and reassembles the file from local cache plus the new chunks.

The key insight: metadata and blobs live in different stores for different reasons. The file tree needs relational joins and strong consistency (renaming a folder is a transaction); the bytes need cheap, durable, content-addressed object storage. Deduplication then falls out for free: a chunk hash that already exists is never uploaded again.


Deep Dives#

1. Chunking#

Instead of uploading a file as one blob, the client splits it into fixed-size chunks (4 MB), hashes each with SHA-256, and represents the file as an ordered list of chunk hashes: the manifest.

Three things fall out of this one decision:

  • Resumable uploads: a failed upload only retries the incomplete chunks, not the whole 10 GB
  • Delta sync: editing one line in a 500 MB file re-uploads one 4 MB chunk, not 500 MB
  • Deduplication: identical chunks (across versions, files, even users) share one stored copy, because identical bytes produce identical hashes

Fixed vs variable chunking. Fixed-size chunking has one weakness: insert a byte at the start of a file and every subsequent chunk boundary shifts, so every chunk hash changes and the whole file looks new. Content-defined chunking (Rabin fingerprinting) picks boundaries based on the content itself, so an insertion only disturbs the chunk it lands in. Dropbox uses this. Fixed-size is the right default to present; name variable chunking as the upgrade.

For senior interviews ↓ content-defined chunking and the boundary-shift problem

Rabin fingerprinting slides a rolling hash over the file and declares a chunk boundary whenever the low k bits of the fingerprint equal a fixed pattern. Because boundaries are anchored to byte patterns, not byte offsets, inserting or deleting bytes shifts only the boundaries local to the edit: the chunks before and after realign to the same content and keep the same hashes.

The trade-offs: chunk sizes become variable (you set a target average, plus min/max bounds to avoid pathological tiny or huge chunks), and the rolling hash costs CPU on the client. On battery-constrained mobile you might disable it and accept fixed chunking's worse delta ratio for insert-heavy files. The payoff is dramatic for exactly the workloads users hit constantly: prepending to a log, inserting a slide into a deck, editing the middle of a large document.


2. The Upload Flow and Deduplication#

The upload is a negotiation: the client proposes chunk hashes, the server says which it already has, and only the genuinely new bytes cross the wire.

code
1. Client splits the file into 4 MB chunks, SHA-256 hashes each
2. Client → Sync Service: "uploading file X, chunks = [h1, h2, h3, ...]"
3. Sync Service checks the Block Store: which hashes already exist?
4. Sync Service → Client: "upload only h2 and h3; h1 already exists"
5. Client uploads h2, h3 directly to S3 via pre-signed URLs
6. Client → Sync Service: "done, here's the full chunk manifest"
7. Sync Service writes a new version row with the manifest (Metadata DB)
8. Sync Service publishes a change event to the Notification Service
9. Other devices receive it and begin pulling new/changed chunks

Step 3 is the deduplication check and the whole trick: a chunk hash that already exists is not re-uploaded: those exact bytes are already in the block store, whether they came from this file, another version, or another user entirely. This is how Dropbox saves enormous amounts of storage and bandwidth. Uploading directly to S3 (step 5) keeps the large-payload path off the Sync Service entirely; the service only ever touches metadata.


3. Delta Sync on Download#

When a device is notified that file X changed, it does the mirror of the upload:

  1. Fetch the new chunk manifest from the Metadata DB
  2. Diff it against the locally cached manifest: which chunk hashes are new?
  3. Download only the new chunks from S3
  4. Reassemble the file from local cache + downloaded chunks

For a large file where one paragraph changed, the device pulls a single 4 MB chunk. The rest is already on disk from the previous version. This is why sync feels instant even on big files and slow connections.


4. Conflict Resolution#

Two devices edit the same file while offline. Both come online with new versions built on different manifests. The Sync Service now has a genuine conflict: neither version descends from the other.

Dropbox's actual strategy: preserve both. The later-arriving version is saved as filename (User's conflicted copy 2026-07-02).ext. Both survive; the user reconciles manually. This is the right interview answer.

Why not auto-merge? Automatic merge only works for known, structured formats (text via operational transforms, as in Google Docs). For arbitrary binary files (a PSD, a spreadsheet, a video) there is no safe automatic merge, and guessing risks corrupting data. Preserving both is the only choice that never loses work.

Last-writer-wins (rejected): simpler, but silently discards one user's changes. Tolerable for single-user-single-device files; unacceptable for shared folders, where it means one person's edits vanish without a trace.

For senior interviews ↓ detecting conflicts without trusting wall clocks

The subtlety is detecting the conflict correctly. You cannot use wall-clock timestamps: device clocks skew, and "later timestamp wins" both loses data and produces false conflicts when a slow clock makes a fresh edit look old.

Use a version vector (or a server-issued monotonic version number per file). Each device tracks the version it last synced from. On upload, the server checks: does the client's edit descend from the current server version? If yes, fast-forward: no conflict. If the client based its edit on version 5 but the server is already at version 7 (another device got there first), the histories have diverged and it's a true conflict → create the conflict copy. This is causal detection: a conflict is precisely two edits neither of which happened-before the other, and version vectors capture happened-before without any clock.


5. Offline Mode#

The client keeps a local SQLite mirror of the metadata for synced folders. Offline changes are recorded as pending operations. On reconnect:

  1. Ask the server for changes since the last synced version
  2. No server changes → upload local pending changes
  3. Server changes exist → run conflict detection (version vectors, per above) and resolve
  4. Apply everything, advance the local synced version

The sync anchor is a server-issued version number (or vector clock) per file, never a wall clock. This is what keeps offline edits from silently clobbering or falsely conflicting with edits that happened elsewhere while the device was dark.


Data Model#

SQL
-- Files: the tree (Postgres, strong consistency for moves/renames)
files (
  id             UUID PRIMARY KEY,
  owner_id       UUID NOT NULL,
  parent_folder  UUID,               -- NULL = root
  name           VARCHAR(255) NOT NULL,
  is_deleted     BOOLEAN DEFAULT FALSE,
  created_at     TIMESTAMPTZ
)

-- Versions: one row per saved version, holds the manifest
file_versions (
  id             UUID PRIMARY KEY,
  file_id        UUID NOT NULL,      -- FK → files.id
  version_number INTEGER NOT NULL,
  chunk_manifest JSONB,              -- ordered list of chunk hashes
  size_bytes     BIGINT,
  device_id      UUID,
  created_at     TIMESTAMPTZ
)
-- Index: (file_id, version_number DESC), fetch latest version
-- Retain last 30 versions; a background job purges older ones

-- Chunks: metadata only; the bytes live in S3
chunks (
  hash        CHAR(64) PRIMARY KEY,  -- SHA-256 hex, content address
  size_bytes  INTEGER,
  s3_key      TEXT NOT NULL,
  ref_count   INTEGER DEFAULT 1      -- for garbage collection
)
-- Block Store (S3): object per chunk, keyed by content hash,
--   sharded prefix (hash[0:2]/hash) to spread request load

ref_count drives garbage collection: when no surviving file version references a chunk, it can be deleted from S3. Decrementing it is what makes 30-day version pruning actually reclaim space.


API Design#

code
POST /api/v2/files/upload/start
─────────────────────────────────────────────
Authorization: Bearer <token>
Request:
  {
    "file_id": "uuid",
    "chunk_hashes": ["sha256...", "sha256...", ...],
    "total_size": 104857600
  }
Response 200:
  {
    "upload_id": "uuid",
    "chunks_needed": ["sha256-2", "sha256-7"],   // dedup result
    "presigned_urls": {
      "sha256-2": "https://s3.amazonaws.com/...",
      "sha256-7": "https://s3.amazonaws.com/..."
    }
  }

POST /api/v2/files/upload/complete
─────────────────────────────────────────────
Request:  { "upload_id": "uuid" }
Response 200: { "version_id": "uuid", "version_number": 4 }

GET /api/v2/files/:id/changes?since=<version>
─────────────────────────────────────────────
Response 200:
  {
    "current_version": 5,
    "chunks_added": ["sha256-8"],
    "chunks_removed": [],
    "presigned_download_urls": { "sha256-8": "https://s3..." }
  }

Note: owner_id and access checks are derived from the auth token server-side; the client never asserts ownership. Pre-signed URLs are scoped to a single chunk and expire quickly, so the large-payload path bypasses the Sync Service without opening a hole.


Failure Scenarios and Edge Cases#

For senior interviews ↓ what breaks and how the system recovers

Upload dies at chunk 7 of 10

Chunking makes this cheap. Chunks 1-6 are already in S3 (idempotent, since re-PUTting the same content hash is a no-op). On retry the client re-negotiates, the server reports only 7-10 as still needed, and the upload resumes. The version isn't committed until upload/complete, so a half-upload never produces a visible corrupt version.

Metadata DB write throughput

Every change writes a version row; at 1,000 uploads/sec with ~10 chunks each that's ~10k chunk-manifest writes/sec. Partition the metadata by owner_id for horizontal scale, batch chunk writes, and keep the hot path (dedup check + version insert) small. The bytes never touch this DB.

S3 request-rate hot prefix

S3 rate-limits per key prefix (~3,500 PUT / 5,500 GET per prefix). Because chunk keys are content hashes, they're already uniformly distributed across the keyspace, so requests spread naturally with no hot prefix. Note that modern S3 also auto-scales and repartitions hot prefixes on its own, so the old "inject a random prefix" trick is largely unnecessary here: the content-hash key already gives you the distribution for free. (It still matters if you ever key objects by something sequential like a timestamp or incrementing ID.)

Notification fan-out for a shared folder

A folder shared with 100 users means every change notifies 100 devices. Fan out asynchronously through a queue (Kafka/SQS) so the upload path isn't blocked waiting on 100 notifications; devices pull the change on their own schedule.

Concurrent edits (the conflict path)

Two offline devices edit the same file. Version-vector detection catches that neither version descends from the other, and the later arrival becomes a conflict copy. No silent data loss; the user reconciles.

Garbage-collecting shared chunks

A chunk is referenced by version 3 of file A and version 5 of file B. Deleting version 3 must not delete the chunk. ref_count guards this: the chunk is only removed from S3 when its ref count hits zero, and GC runs as a background sweep, not inline with delete.

Clock skew producing false conflicts

If you ordered by wall clock, a device with a slow clock would make fresh edits look stale and trigger phantom conflicts. Version numbers / vector clocks remove wall clocks from the decision entirely.


Code#

Chunked Upload with Deduplication#

Java
public class SyncClient {
    private static final int CHUNK_SIZE = 4 * 1024 * 1024;   // 4 MB
    private final SyncService sync;
    private final S3Client s3;

    public UploadResult upload(File file, UUID fileId) throws IOException {
        // 1. Split into chunks and content-hash each one.
        List<Chunk> chunks = chunkAndHash(file);              // SHA-256 per chunk
        List<String> hashes = chunks.stream().map(Chunk::hash).toList();

        // 2. Ask the server which chunks are actually new (dedup).
        UploadStart start = sync.startUpload(fileId, hashes, file.length());

        // 3. Upload ONLY the new chunks, straight to S3 via pre-signed URLs.
        for (String needed : start.chunksNeeded()) {
            byte[] data = chunkByHash(chunks, needed);
            s3.put(start.presignedUrl(needed), data);         // bytes skip our service
        }

        // 4. Commit the version with the full manifest.
        return sync.completeUpload(start.uploadId());          // → new version_number
    }
}

Trade-off Summary#

DecisionChosenAlternativeWhy
Transfer unit4 MB chunksWhole-file uploadResumable, delta sync, and dedup all fall out of chunking
Chunk sizeFixed 4 MBVariable (Rabin)Simpler default; variable handles insertions better, offer as upgrade
Chunk addressingContent hash (SHA-256)Random UUIDIdentical bytes → identical key → free deduplication across users
Conflict strategyPreserve both (conflict copy)Last-writer-winsNever silently lose a user's work; only safe choice for binary files
Conflict detectionVersion vectorsWall-clock timestampsCausal, skew-proof; wall clocks lose data and fake conflicts
Metadata storePostgreSQLDynamoDBRelational joins + transactions for the file tree; strong consistency
Blob storeS3Self-hosted HDFSManaged, durable, globally available, no ops overhead

Follow-up Questions#

Basic

Q: Why chunk files instead of uploading them whole?

Three payoffs from one decision: resumable uploads (retry only the failed chunk), delta sync (edit one line in a 500 MB file, upload one 4 MB chunk), and deduplication (identical chunks share one stored copy because their hashes match). Whole-file upload gives you none of these and re-sends gigabytes for a one-byte change.

Q: How does deduplication work, and where does the saving come from?

Each chunk is addressed by the SHA-256 of its contents. Before uploading, the client asks the server which hashes already exist; existing ones aren't re-uploaded because those exact bytes are already in the block store. The same 4 MB block appearing in two files (or two users' accounts) is stored once. Industry benchmarks put duplicate chunks at 20-40%.

Q: Why split metadata and blobs into two different stores?

They have opposite requirements. The file tree needs relational joins and transactional consistency: renaming a folder or moving a file must be atomic. The bytes need cheap, durable, content-addressed storage at exabyte scale. Postgres is right for the first, S3 for the second; forcing either to do both job is where designs go wrong.

Q: What happens on a conflict?

Preserve both versions. The later arrival is saved as a conflict copy (filename (conflicted copy).ext) and the user reconciles. Auto-merge is only safe for structured formats like text; for arbitrary binary files, guessing risks corruption, so keeping both is the only choice that never loses work.

Senior follow-ups ↓

Q: A user inserts one byte at the very start of a 1 GB file. With fixed chunking, what happens, and how do you fix it?

With fixed 4 MB chunks, every boundary shifts by one byte, so every chunk hash changes and the client re-uploads the entire 1 GB, and dedup buys nothing. The fix is content-defined chunking (Rabin fingerprinting): boundaries are anchored to byte patterns, not offsets, so only the first chunk changes and the rest realign to their old hashes. You'd re-upload ~4 MB instead of 1 GB. It costs client CPU for the rolling hash, which is why mobile may keep fixed chunking.

Q: Two chunks from two different users happen to be identical. Is cross-user dedup safe?

Storage-wise, yes: identical bytes, one copy, guarded by ref_count so neither user's delete removes the other's data. The concern is privacy/security: naive existence checks can leak information (a client learns a chunk already exists, inferring another user has that exact content, a confirmation attack on known files). Mitigations: scope dedup per-account for sensitive tiers, or add server-side per-user encryption so identical plaintext yields different ciphertext (at the cost of losing cross-user dedup). It's a real trade-off between storage efficiency and information leakage.

Q: How do you garbage-collect chunks when versions are pruned, without deleting live data?

Reference counting. Each chunk row has a ref_count of how many file versions point at it. Pruning a version decrements the counts for its chunks; a chunk is eligible for deletion from S3 only when its count reaches zero. GC runs as a background sweep, not inline with the delete, and must handle the race where a new version references a chunk mid-sweep: typically a grace period or a mark-and-verify pass before the actual S3 delete.

Q: The interviewer asks for real-time collaborative editing on top of this. What fundamentally changes?

Chunk-level sync is too coarse: you can't merge two people typing in the same paragraph by swapping 4 MB blocks. You move to character/operation-level sync with Operational Transforms or CRDTs, a persistent connection instead of poll-and-notify, and server-side conflict resolution (merge) rather than conflict preservation (copy). It's essentially a different system layered above storage; the chunked file sync becomes the durability/snapshot layer beneath the live collaborative session.


Minute-by-Minute Interview Playbook#

0 to 5 min: Requirements Ask the questions above. The sync-vs-collaboration distinction and the max file size shape everything. Confirm versioning, sharing, and out-of-scope (no real-time co-editing). Write FR/NFR on the board.

5 to 8 min: Capacity estimates Do the math out loud, then reframe: storing bytes is solved, bandwidth is the real constraint. That reframing justifies chunking as the centerpiece rather than an afterthought.

8 to 18 min: HLD Draw the sync client, Sync Service, Metadata DB, and Block Store. Walk the happy path: edit → client chunks and hashes → dedup negotiation → upload new chunks to S3 → commit version → notify → other devices delta-download.

18 to 38 min: Deep dives Lead with chunking: it's the highest-value insight. Explain why whole-file upload is wrong, then the upload flow with the dedup check, then delta sync on download, then conflict resolution (conflict copy, and version vectors not wall clocks for detection).

38 to 45 min: Failure scenarios and wrap-up Cover resumable uploads, ref-count GC, and shared-folder notification fan-out. Summarize in one sentence: "Chunk files, address chunks by content hash for free dedup, sync only the delta, split metadata from blobs, and preserve both versions on conflict."

Green flags

  • Proposes chunking immediately instead of whole-file upload
  • Recognizes content-addressed storage makes dedup a free side effect
  • Separates the metadata DB (file tree) from the blob store (chunks) and says why
  • Has a clear, safe conflict policy and detects conflicts with version vectors, not clocks
  • Mentions Rabin fingerprinting as the fix for the insertion problem

Red flags

  • Stores files as BLOBs in a relational database
  • No delta sync: "just re-upload the file when it changes"
  • Last-writer-wins conflict resolution without acknowledging silent data loss
  • Doesn't separate metadata from blob storage
  • Never mentions deduplication

Further Reading#

Streaming File Synchronization at Dropbox
How Dropbox transfers only the changed blocks of a file: the delta-sync protocol and the engineering behind the bandwidth savings.
Inside the Magic Pocket
Dropbox's exabyte-scale content-addressed block storage system: how the block store underneath chunking actually works in production.
The rsync Algorithm
The foundational delta-transfer algorithm: rolling checksums and content-defined boundaries that inspired modern file sync.
Related: Design a URL Shortener
Another storage-and-retrieval problem; the content-addressing and hashing mindset from Dropbox carries straight over.
Related: Design a News Feed
The async fan-out for shared-folder notifications is the same push pattern that drives feed delivery at scale.

Try a different category