Skip to content

The finished design, decision by decision

How to design a Dropbox-Style File Sync Service

Not the one correct diagram, but a design you can defend under these constraints: the finished architecture, then every stage's question with the reasoning that answers it, the tradeoffs it accepts, and where another engineer could land differently.

Keeping files in sync across devices

The short answer

7 parts, each with one job. The map below shows how requests and data move between them; the stages after it explain why each part is there.

Desktop and mobile clients
Watch local files, hash content, persist upload state and sync cursors, and apply authorized remote changes.
File API
Authenticates each operation, checks current access, validates preconditions and routes metadata or block requests.
Metadata service
Owns namespaces, paths, versions, permission checks and the append-only change journal.
Metadata database
Stores current metadata and a durable, ordered journal partitioned by namespace.
Immutable block store
Stores verified block bytes by content hash; blocks are written once and read by authorized clients.
Change notifier
Wakes online devices when a namespace journal advances; missed notifications are recovered from the journal.
Garbage collector
Marks blocks reachable from retained versions, then deletes only blocks that remain unreferenced after a safety delay.
1234567CLIENTDesktop andmobile clientsEDGEFile APISERVICEMetadata serviceDATABASEMetadatadatabaseOBJECT STOREImmutableblock storeSERVICEChange notifierWORKERGarbagecollector

Select a component to see what it is responsible for and which state it owns.

  1. 1Desktop and mobile clients → File API: Upload, download and sync requests
  2. 2File API → Metadata service: Conditional metadata operations
  3. 3Metadata service → Metadata database: Commit version and append journal row
  4. 4File API → Immutable block store: Upload or fetch verified blocks
  5. 5Metadata database → Change notifier: Journal advanced
  6. 6Change notifier → Desktop and mobile clients: Wake, then page from cursor
  7. 7Garbage collector → Immutable block store: Delete unreachable blocks after grace period
  • Request / response
  • Bulk data
  • Asynchronous
  • Server push

Why does this design work?

The service has one mutable control plane and one immutable data plane. Devices send verified blocks to durable storage, then atomically publish a version manifest and append a namespace change. Ordered cursors let online and long-offline clients converge; conditional writes expose concurrent edits; current authorization governs both metadata and block reads; delayed mark-and-sweep reclaims content only after every retained reference is gone.

Invariants, and where they are enforced

  • A visible file version references only blocks that were fully received, hash-verified and durably stored first.

    Upload missing blocks first; then use a conditional metadata transaction to commit the manifest and append its change row. A crash before commit leaves harmless unreferenced blocks; a crash after commit cannot leave missing bytes. Enforced by File API, Metadata service, Metadata database, Immutable block store.

  • A client can resume after any interruption without skipping a committed namespace change.

    Each namespace has a monotonic journal sequence. Page all rows after the client's durable cursor, and advance that cursor only after applying a whole page. Enforced by Metadata service, Metadata database, Desktop and mobile clients.

  • A revoked user cannot use an old metadata result or download URL to fetch a block indefinitely.

    Authorize block reads against current file or namespace policy; if short-lived signed URLs are used, bound their lifetime and account for revocation propagation. Enforced by File API, Metadata service, Immutable block store.

  • Garbage collection never deletes a block reachable from a retained or newly restored version.

    Trace references from all retained manifests, recheck before deletion, and wait through the maximum in-flight upload, restore and metadata replication delay. Enforced by Metadata service, Metadata database, Garbage collector, Immutable block store.

What does it rely on?

  • Block writes are durable before a manifest can reference them.
  • A metadata transaction checks the client's expected revision and appends the corresponding journal row atomically.
  • Devices persist an applied journal cursor and can replay pages idempotently.
  • Permission checks, cached metadata and download grants obey the same revocation window.
  • The garbage collector sees every retained manifest and rechecks before deleting blocks.

What tradeoffs does it make?

ChoiceGainsCosts
Immutable content blocksRetryable writes, content reuse and simple integrity checks.More metadata and asynchronous garbage collection after deletes or replacement.
Ordered namespace journalEfficient incremental catch-up and one source of truth for changes.Cursor retention, pagination and snapshot recovery need explicit contracts.
Conflict copies for unknown formatsAvoids silently losing offline work.Users may need to resolve duplicate files; byte-level merging is unavailable.
Short-lived signed downloadsEfficient direct reads from object storage.Revocation is bounded by URL expiry, not instantaneous.

What are the reasonable alternatives?

Whole-file objects with versioned keys
Better when files are small, edits usually replace the whole file, and simplicity matters more than bandwidth reuse.
Fixed-size content blocks
Better when files are large, transfer resume matters, and edits tend to preserve aligned regions.
Content-defined chunking
Better when insertions shift later byte offsets and the storage savings justify more CPU and manifest complexity.
Format-aware merge
Better when the product owns a structured format with well-defined merge rules; otherwise preserve both versions.
Proxy every block download
Better when fast revocation and centralized policy outweigh the extra proxy bandwidth and latency.

When does it stop working?

  • The product requires simultaneous character-by-character editing; whole-file version conflicts are no longer an adequate collaboration model.
  • Very small files dominate so completely that one block and journal row per file overwhelms metadata; batching and a different small-object layout may be needed.
  • The required revocation time is shorter than the shortest practical signed URL lifetime; reads need a revocable authorization path.
  • Journal retention is shorter than the longest supported offline period and the service cannot construct a correct snapshot.
  • Hashing, deduplication or encryption assumptions expose cross-tenant equality or side channels; isolation and threat modeling change the storage key design.

Every stage, decided and explained

Spoilers, for the whole investigation: each stage's question and its answer, the reasoning behind it, and the tradeoffs it accepts. If you have not worked through the stages yet, you may want to do that first.

Work through the stages

Stage 1 of 7 · Model

The bytes behind a sync

Use 86,400 seconds per day. The stated 2 MB average is after compression; it is deliberately a first estimate, not a claim about a production file distribution.

What you need to know first

20 million changed files averaging 2 MB each produce how much new content per day, before deduplication?

About 40 TB.

20,000,000 × 2 MB = 40,000,000 MB, or about 40 TB per day in decimal units. Replication, metadata, old versions and retries add more; deduplication may reduce stored bytes but should not be assumed for capacity planning.

If those 40 TB arrive evenly over a day, what average payload bandwidth does the service receive?

About 463 MB/s.

40 TB ÷ 86,400 seconds ≈ 463 MB/s aggregate, before protocol overhead and peak-to-average variation. This is a fleet-wide flow, not what one API server should stream through its request memory.

The workload has two different costs. Small files are dominated by request, hashing and metadata overhead; large files are dominated by bytes on the wire and interruption risk. A design that treats every file as one HTTP body pays both costs badly.

What the stage asks

Which conclusions follow from the workload?

  1. Holds

    At the given average, the service receives about 40 TB of changed file content per day before deduplication.

    20 million × 2 MB is 40 TB in decimal units. Replication and retained history affect storage, while bursts affect peak bandwidth.

  2. Fails

    A single request carrying the entire file is a good default because it uses fewer API calls.

    It cannot resume a multi-gigabyte transfer after a timeout, and it ties API connection lifetime and memory pressure to file size. Small files may still be sent in one request; large transfers need bounded resumable pieces.

  3. Depends

    Content-addressed blocks guarantee that storage will be much smaller than the source bytes.

    Identical blocks can be reused, but encrypted or already-compressed data may differ even when files look similar. Capacity planning should use measured deduplication, not assume it.

The reasoning

  1. The average input alone is about 40 TB/day before deduplication.
  2. Large transfers need bounded retries; small files need low per-request overhead.
  3. Treat measured deduplication as an optimization, not a durability assumption.

Sizing establishes why bytes and metadata should take different paths. A few hundred megabytes per second of average payload is a system-level budget; each API process should validate and authorize transfers without buffering whole files. Peak load and geographic egress still need explicit targets before choosing regions or hardware.

Stage 2 of 7 · Decide

Store content once, describe versions separately

A user edits a 2 GB video, changing a small region near the end. The first version stores the whole file as one mutable object. The next version must avoid retransmitting unchanged content and must remain readable while the new one is being uploaded.

What you need to know first

Split file bytes into bounded blocks and identify each block by a cryptographic hash. A version manifest stores an ordered list of block IDs plus file metadata. Immutable blocks are simple to retry and cache; mutable meaning lives in the version manifest, not inside the block store.

Which representation best supports large-file edits and retries?

Store verified immutable blocks by hash; publish a version manifest that references them.

Unchanged blocks can be reused, retries are idempotent by block hash, and the old manifest remains the visible version until the new manifest commits.

For a large upload, the client first sends its proposed block list. The service returns which hashes are already durable; the client uploads only the missing blocks with bounded concurrency. After a disconnect, it can repeat that negotiation and resume. The server does not need to trust a remembered session to know which bytes exist: it checks the durable block index again.

Content hashes are identifiers and integrity checks, not authorization tokens. A block store still needs a policy for who may read a hash; otherwise a guessed or leaked identifier can expose data across users.

What the stage asks

A client retries a block upload after losing the response. Choose the server behavior.

  1. Sound

    Verify the bytes against the claimed hash, then treat an existing matching block as success.

    The hash makes the write idempotent, and verifying it protects against corruption or a client bug.

  2. Flawed

    Trust the client's hash and overwrite whichever bytes are stored under it.

    A key is not proof that the bytes match. Overwriting also breaks immutability and could corrupt old manifests.

  3. Flawed

    Create a fresh random block ID on every retry.

    The retry creates duplicate content and the client has no stable identifier to resume with.

What a strong answer covers

  • Reuses the content hash as a stable idempotency key.
  • Checks received bytes against the hash before accepting them.
  • Does not overwrite a different existing block under the same key.

The reasoning

  1. Keep immutable bytes separate from mutable file/version metadata.
  2. A content hash supports reuse and retry, but the server must verify it.
  3. A manifest is the atomic switch that makes a new version visible.

Block size is a trade-off: smaller blocks improve reuse after localized edits but increase hashes and metadata; larger blocks reduce metadata while retransmitting more unchanged bytes. Dropbox has publicly described blocks up to 4 MiB in specific systems, but that is evidence about their workload, not a universal optimum. Choose from this product's edit patterns and measure.

Stage 3 of 7 · Break it

Make the file appear only when it is complete

The client knows the desired block hashes. Some blocks already exist; two are missing. The file must not become visible with missing data. The client may retry after an unknown number of successful writes.

What you need to know first

When should the service publish a new version manifest?

After all missing blocks have been verified and durably stored, using a conditional metadata transaction.

The visible manifest can now resolve every block. A crash before commit leaves the previous version intact and only harmless orphan bytes.

The safe sequence is: authenticate and validate the expected current revision; upload missing blocks and wait for durable acknowledgements; conditionally commit the new manifest and append its journal row in one metadata transaction; then notify online devices. If the process crashes before commit, the previous version is still visible and the new blocks are harmless orphans. If it crashes after commit, every referenced block was already acknowledged as durable. Notifications follow the commit and can be retried.

The commit should compare the current file revision with the revision the client edited. This is a compare-and-swap: only one writer based on revision 12 can publish revision 13. A second writer gets a conflict response and can preserve its content as a separate version instead of silently replacing the winner.

What the stage asks

The database recovers after the upload failure. Which statements are correct?

  1. Holds

    Blocks uploaded before the metadata transaction may be unreferenced, but the previous file version remains readable.

    Immutable blocks are not user-visible by themselves. Without a committed manifest, readers continue to resolve the old version.

  2. Fails

    Commit the manifest first and upload its missing blocks afterward to shorten the metadata transaction.

    A crash or storage outage after the commit leaves a visible version whose manifest points to missing bytes.

  3. Holds

    If the push notification is lost, the other device can still discover the update.

    Notifications wake clients; the durable journal is the source of truth. The client resumes from its cursor even if it missed the wake-up.

The reasoning

  1. Store and verify all referenced blocks before committing the new manifest.
  2. Use a conditional revision check to detect concurrent writes.
  3. Push is a hint; the metadata journal is the durable change record.

No transaction spans object storage and the metadata database. Correctness comes from choosing an order where the partial failure leaves harmless orphan data rather than a broken visible file. Idempotent block writes make retries safe; a later mark-and-sweep collector handles abandoned blocks.

Stage 4 of 7 · Decide

Catch up from a durable cursor

A laptop has been asleep for three weeks. During that time its owner changed 80 files and a shared folder received 12,000 changes. The phone is online and expects updates within a few seconds.

What you need to know first

Give each namespace an ordered journal and each device a durable cursor. A client requests changes after its cursor in bounded pages, applies them, then saves the cursor. If it crashes while applying a page, it can safely repeat it using stable change IDs and idempotent local operations.

What should a reconnecting device do after missing notifications?

Page through durable journal entries after its saved cursor, then fetch blocks for changed versions.

Work scales with missed changes, and the same journal handles both push wake-ups and long-offline recovery.

A cursor expires only if the server has compacted the journal past it. Make that response explicit: tell the client to rebuild from a snapshot at sequence S, then continue after S. Silently returning only recent changes would leave the device permanently inconsistent.

What the stage asks

The client applied changes through sequence 812 but crashed before saving its cursor, still 790. What is safe on restart?

  1. Sound

    Request after 790 again and reapply the overlapping page idempotently.

    At-least-once replay is safe when changes have stable IDs and local application records or tolerates duplicate application.

  2. Flawed

    Jump to the server's current high-water mark to avoid duplicate work.

    That skips every change after 790 that the client had not actually committed locally.

  3. Flawed

    Use the file modification times on disk to reconstruct the missing cursor.

    Wall clocks can differ, and local timestamps do not represent the server's ordered metadata history.

What a strong answer covers

  • Advances the cursor only after the page's local changes are durably applied.
  • Allows overlap and makes repeated change application idempotent.

The reasoning

  1. Notifications wake devices; durable cursors let them recover missed changes.
  2. Paginated journals make reconnect work proportional to the change backlog.
  3. An expired cursor needs a snapshot-and-resume protocol, not silent truncation.

A cursor is a compact promise about history: the client has durably applied every change through sequence N. Advance it only with the corresponding local state. If the journal is partitioned by namespace, clients can catch up on independent namespaces in parallel while preserving order within each one.

Stage 5 of 7 · Break it

Two offline edits are both real work

The files can be arbitrary binaries: text documents, photos, archives or video. The server does not know whether merging bytes would preserve the user's intent.

What you need to know first

A last-write-wins update silently discards one valid edit. Compare the client's base revision to the current revision at commit. If they differ, preserve both versions or create an explicit conflict copy. A product-specific merge is appropriate only when the format has a safe, well-defined merge algorithm.

Both versions changed since revision 12. What is the default for arbitrary files?

Keep the current version and store the other as a named conflict copy linked to its base revision.

Both edits remain recoverable. A text-aware merge can be added for formats where the merge semantics are explicit.

What the stage asks

Which statements are safe design choices?

  1. Holds

    A conditional write on expected revision detects that a device edited stale content.

    The metadata database can atomically compare the current revision before publishing a new manifest.

  2. Fails

    Because each file is split into blocks, independently changed blocks can always be combined safely.

    Block boundaries are a storage optimization, not semantic merge boundaries. Reordering bytes, archives and encrypted files can make block-level combinations corrupt the file.

  3. Holds

    A format-aware merge can be offered for specific file types while preserving conflict copies as a fallback.

    The application can apply known merge semantics to compatible text or structured documents, but must retain both inputs if the merge is ambiguous or invalid.

The reasoning

  1. Compare the base revision at commit; do not trust device arrival order.
  2. Preserve both edits when their semantics are unknown.
  3. Block boundaries do not imply safe merge boundaries.

This exercise excludes live collaborative editing. That changes the right design: a file-sync service sees whole versions after offline work, while a collaborative editor needs an operation log and a merge model for concurrent edits. Do not silently borrow one system's consistency contract for the other.

Stage 6 of 7 · Change it

Sharing is an authorization check on every path

A file can be reached through a namespace path, a version manifest and many content blocks. Removing a user from the shared folder must not leave a permanent capability to keep downloading those blocks.

What you need to know first

Check authorization at the metadata operation and at block retrieval. A signed URL is a bearer capability: anyone who has it can use it until it expires. Short lifetimes reduce revocation delay; proxying reads through an authorization service gives faster revocation but adds bandwidth, cost and a dependency on the hot path.

The requirement says revocation takes effect within one minute. What must be true of cached download grants?

Keep grants under one minute, or proxy block reads through a current authorization check.

Either approach bounds the stale permission window to the stated contract, with a clear trade-off between latency/cost and revocation speed.

What the stage asks

Trace a block download after a user is removed from a shared folder. Name the checks and state the maximum revocation delay your design promises.

Reference answer

The download path authenticates the user, resolves current access to the file's namespace/version, then reads the requested block. If direct object-store URLs are required, expire them within the one-minute promise and prevent old grants from being refreshed without a new authorization check. A metadata cache can be used only if its invalidation or TTL obeys the same bound.

What a strong answer covers

  • Authenticates the requesting user and resolves the current namespace membership.
  • Checks access to the file or version before returning its block data.
  • Bounds cached or signed grants to the stated revocation window, or validates each request.

The reasoning

  1. A content hash is not an access-control check.
  2. Direct signed links remain usable until expiry; set expiry from the revocation promise.
  3. Metadata caches and block caches must share the security contract.

Fast revocation and cheap direct downloads pull in opposite directions. State the allowed delay first. Then choose a bounded signed URL, an authorization-aware edge, or a proxy, and test behavior for cached metadata and already-issued capabilities.

Stage 7 of 7 · Defend it

Delete now; reclaim bytes only when safe

Deleting one path may not make its blocks unreachable: another version or another file may reference the same content. A restore can also race with a cleanup job that previously saw no references.

What you need to know first

Make delete a metadata operation first: append a tombstone, remove the version from the active namespace, and preserve it for the product's retention window. A collector finds blocks unreachable from all retained manifests, but a stale mark is not permission to delete immediately. Recheck reachability and wait beyond the maximum metadata replication and in-flight upload delay.

At large scale, immutable blocks simplify reads and retries but move complexity into reclamation. Garbage collection identifies unreferenced content; compaction is the physical work that makes space reusable. Dropbox's public Magic Pocket posts describe this trade-off in its own storage system; the exercise uses it as an operational lesson, not as a claim that this design matches their implementation.

What the stage asks

The collector marked block H unreferenced yesterday. What can it safely do today?

  1. Fails

    Delete H immediately because yesterday's reference scan found zero owners.

    A restore, delayed metadata replica or in-flight commit may have made H reachable after the scan. Recheck against current authoritative metadata and honor the safety delay.

  2. Holds

    Keep H while any retained version manifest still references it, even if the current path was deleted.

    The product's restore window is part of reachability. Expire the version explicitly before its blocks become collectible.

  3. Holds

    Recheck reachability at deletion time and make the delete operation safe to retry.

    The final decision must use current metadata, and an idempotent delete handles worker retry after an uncertain response.

The reasoning

  1. A user-visible delete is a metadata tombstone; physical reclamation comes later.
  2. Count all retained manifests and shared references before deleting a block.
  3. Recheck reachability after the grace period; make cleanup idempotent.

A clean separation of content and metadata helps this service grow, but it does not make storage free. Version retention, deduplication and garbage collection form one policy: retaining history saves user work and costs capacity; aggressive reclamation saves disks and increases the chance of deleting something users expect to restore. Measure live bytes, unreachable bytes, compaction throughput and restore success together.

How you did

Now try it as an interview question

  • “Design Dropbox or Google Drive file storage and synchronization.”
  • “Design a cloud drive that resumes multi-gigabyte uploads over unreliable networks.”
  • “How should a desktop client sync after being offline for several weeks?”
  • “How do you handle two devices editing the same file while offline?”
  • “Design file sharing with version history, deletion and fast permission revocation.”

The timed session in Review mixes stages from this and other systems with concept recall and questions about your own projects.

For concise outlines of the prompts, see the system design interview questions and answers.

Back to the last stage