Prompt and applicable scenarios
Design a cloud file storage and multi-device sync service similar to Dropbox or Google Drive. It has 50 million registered users and 5 million daily active users. Each user stores 10 GB of logical data on average. The system receives 100 million new file versions per day, with 4 MB of new or changed content per version. Peak traffic is five times the daily average. A file can be as large as 50 GB, online devices should observe creates, updates, moves, and deletes within 5 seconds, and the metadata service targets 99.99% availability.
The scale, latency, availability, and 4 MiB target chunk size are interview assumptions, not public performance commitments from any storage product. The scope includes upload, download, resumable transfer, version recovery, multi-device sync, offline editing, conflict copies, simple sharing, and deletion. Character-level collaborative editing, semantic Office document merging, a complete enterprise authorization system, and active-active cross-region writes are out of scope.
Public system design prompts in 2026 still use Dropbox, Google Drive, or a generic file sync service to ask about large-file chunking, delta sync, versions, offline operations, and conflicts. Official object-storage documentation also establishes the engineering value of multipart upload: parts can be uploaded in parallel and a failed part can be retried independently, while abandoned parts need an abort or lifecycle cleanup. The interview is about separating content transfer from namespace state and closing the correctness loop around recovery and deletion, not memorizing one company's internal architecture.
What the interviewer evaluates
First, can the candidate separate file content from metadata? Large byte sequences belong in durable, inexpensive, immutable object storage. Names, parent-child relationships, current versions, deletion markers, and sync cursors require conditional writes and ordered changes. Putting a 50 GB file in a relational database, or using object keys as the complete directory model, makes updates, moves, transaction commits, and recovery fragile.
Second, is there one explicit publication point for an upload? A client may upload missing chunks in parallel, but the version becomes visible only after the server verifies every chunk and atomically updates currentVersionId with the change log. Otherwise, metadata may reference missing content, or uploaded bytes may never become visible to the user.
Third, can the sync protocol survive lost, duplicate, and reordered notifications? A push message is only a hint that something changed. A device must use a durable cursor to pull authoritative changes. Devices that were offline for days, reinstalled clients, and expired cursors all need a recovery path. A WebSocket connection is not the source of truth.
Fourth, are the concurrency semantics honest? When two offline devices modify the same ordinary binary file from the same old version, a generic service cannot reliably merge them. A failed conditional commit should preserve both results and create a conflict copy instead of silently using last-writer-wins. Dropbox's public help documentation likewise explains that simultaneous or offline edits can produce a conflicted copy that users must merge.
Finally, does the answer connect capacity, cost, and reclamation? The candidate should distinguish logical capacity from deduplicated physical capacity, estimate version commits and byte throughput, and explain orphan chunks, retained versions, deletion tombstones, quotas, hot namespaces, and why garbage collection cannot rely on one instantaneous reference count.
Clarifying questions before answering
- What content is synchronized? Ordinary files and directories; no character-level live collaboration.
- What consistency is required? The uploading device gets read-your-write behavior; other online devices converge within 5 seconds.
- How long is incremental history retained? Assume 30 days; older cursors require a namespace snapshot before resuming deltas.
- How are concurrent edits handled? Only one commit from a given
baseVersionIdbecomes current; the later result is retained as a conflict copy. - Can deletion reclaim bytes immediately? No. Write a tombstone and retain it for 30 days for offline sync and recovery.
- Is deduplication global across users? Default to account- or tenant-scoped deduplication to reduce content-existence leaks and encryption coupling.
- What sharing is in scope? Read-only links for a file or directory; complex organization permissions are a follow-up.
- How are encryption and malicious files handled? Encryption in transit and at rest, short-lived signed URLs, and asynchronous malware scanning; end-to-end encryption is out of scope.
- How do regions accept writes? Each namespace has one home write region; objects may replicate across regions and metadata has asynchronous disaster-recovery replication.
- Is an instant-upload hit guaranteed? No fixed deduplication ratio is assumed. Charge logical quota by user-visible size and measure physical savings separately.
30-second answer framework
"I would store immutable chunks of about 4 MiB in object storage and keep directories, current versions, manifests, tombstones, and ordered changes in strongly consistent metadata. Clients upload only missing chunks, then atomically commit with baseVersionId and an idempotency key. Devices persist a cursor and use notifications only to trigger /changes?cursor=, so a lost notification does not lose data. A concurrent offline commit preserves a conflict copy. At 100 million versions per day, five-times peak is about 6,000 commits/s, while 400 TB of daily ingress peaks near 25 GB/s. Deletes retain tombstones, and chunks are reclaimed only after a grace period and manifest reconciliation."
Step-by-step deep dive
Start with five invariants:
- A published version references only chunks that exist and passed integrity checks.
- A commit replaces
currentVersionIdonly whenbaseVersionIdstill equals the current version. - Change sequence numbers strictly increase within a namespace, and device cursors only advance.
- A delete writes a tombstone; data that may still be referenced is not physically deleted during the sync and recovery window.
- Garbage collection deletes a chunk only after a grace period and manifest reconciliation still show no references.
Step one: separate the client, metadata plane, and content plane.
The client has a file watcher, local index, durable operation journal, chunker, and sync engine. After a crash, it recovers which chunks were uploaded and which commit is uncertain instead of rescanning and uploading every file. The server has an API gateway, authentication and quota checks, a metadata service, upload coordinator, object storage, namespace change log, notification service, download CDN, malware scanner, and garbage collector.
Metadata routes by namespaceId. A personal drive is one namespace, and a shared folder can become another, keeping authorization, ordered changes, and hotspot isolation inside one boundary. Each namespace initially has one home write region with synchronous metadata replication across availability zones. Downloads may use a nearby CDN or object replica. This avoids two regional leaders concurrently accepting conflicting directory updates.
Step two: define the data model.
FileEntry(id, namespaceId, parentId, name, type, currentVersionId, deletedAt)
FileVersion(id, fileId, baseVersionId, size, manifestHash, createdBy, createdAt)
VersionChunk(versionId, ordinal, chunkHash, size)
UploadSession(id, fileId, baseVersionId, state, expiresAt, idempotencyKey)
Change(namespaceId, seq, entityId, operation, versionId, createdAt)The directory tree uses stable fileId and parentId values, so a move or rename changes metadata without copying content. A FileVersion is immutable, and ordered VersionChunk rows form its manifest. manifestHash validates the manifest but does not replace each chunk's checksum. UploadSession stores session state and its idempotency key. Change.seq is monotonic within a namespace, and a delete is another logged operation.
Enforce name uniqueness with a conditional constraint on (namespaceId, parentId, normalizedName). The product must define case normalization explicitly; otherwise Windows, macOS, and Linux clients can disagree about whether two names conflict.
Step three: design resumable multipart upload.
POST /files/{fileId}/upload-sessions
{ baseVersionId, size, chunks[], idempotencyKey }
-> { uploadSessionId, missingChunks[], signedUrls[] }
PUT {signedChunkUrl}
Content-Checksum: ...
POST /upload-sessions/{uploadSessionId}/commit
{ manifestHash, idempotencyKey }
-> { fileVersionId, changeSeq }
GET /files/{fileId}/download-manifest
-> { fileVersionId, chunks[], signedUrls[] }The client starts with chunks of about 4 MiB and computes a strong hash. The server looks for existing chunks only within the account or tenant and issues short-lived, object- and operation-scoped signed URLs for missing chunks. The client uploads with bounded parallelism and retries only failed parts. Official AWS and Alibaba Cloud multipart documentation both use an initiate, upload-parts, and complete session model. They also note that unfinished parts continue using storage, so sessions need expiration and long-abandoned uploads need an explicit abort.
Fixed-size chunks are simple and parallelize well for most files. If the workload frequently inserts a few bytes near the beginning, every later fixed boundary shifts and the hashes change. Content-defined chunking can recover more unchanged data at the cost of CPU and implementation complexity. Start with fixed chunks and upgrade only when observed edit patterns justify it.
Step four: make version commit the only publication boundary.
The commit endpoint first looks up an existing result by idempotencyKey, then validates chunk sizes, hashes, authorization, and quota. In one metadata transaction, it:
- Locks or conditionally reads
FileEntry.currentVersionId. - Verifies that it still equals the request's
baseVersionId. - Writes the immutable
FileVersionandVersionChunkmanifest. - Updates
currentVersionId. - Appends the next
Change.seqand a transactional outbox record.
Object bytes complete before the metadata transaction, so the transaction cannot publish a reference to a missing chunk. A notification failure after commit does not affect correctness: the outbox retries, and devices can still pull by cursor. If the commit response is lost, retrying the same idempotency key returns the original fileVersionId instead of creating another logical version or charging quota twice.
Step five: synchronize devices with cursors.
GET /changes?namespaceId={id}&cursor={lastSeq}&limit=1000
-> { changes[], nextCursor, hasMore }A device stores lastSeq and its local file index in the same durable transaction. After a "namespace may have changed" notification, it pulls until hasMore=false, applies creates, updates, moves, and tombstones in order, and only then commits the new cursor. seq makes replay idempotent. Reordered notifications do not affect the pull result. If every notification is lost, foreground wakeups and periodic checks still discover a newer sequence.
If the cursor is older than the 30-day retention window, the service returns cursor_expired with a location for a consistent namespace snapshot. The client loads the snapshot, reconciles local uncommitted operations, and resumes from the snapshot watermark. It must not simply delete all local files. Notifications are a latency optimization; the cursor log is the sync protocol.
Step six: handle conflicts, deletion, and version recovery.
Devices A and B both edit version 10 offline. A commits version 11 with baseVersionId=10. B's later conditional commit fails. The service retains B's uploaded content, creates a new entry such as "name (B's conflicted copy)" or a conflict version, and appends a namespace change. Ordinary binary files are not silently auto-merged. Text- or document-specific merging is a separate product capability.
A delete updates deletedAt and appends a tombstone. Online devices move the entry to trash, and an offline device can learn about the delete when it reconnects. Historical versions and chunk references remain valid during the 30-day recovery window. Afterward, the collector computes the set of chunks referenced by retained manifests, marks unreferenced candidates, waits a grace period, and reconciles again before deleting. Retries, delayed events, and repair jobs can make an instant reference count wrong, so it cannot be the sole authority for irreversible deletion.
Step seven: estimate capacity and isolate hotspots.
50 million × 10 GB = 500 PB of logical storage. Replication, history, and deduplication change physical capacity, but the prompt gives no ratios, so a precise physical number would be invented. Daily logical ingress is 100 million × 4 MB = 400 TB, averaging about 4.6 GB/s and peaking around 25 GB/s. Version commits average 100,000,000 / 86,400 ≈ 1,157/s and round to about 6,000/s at five-times peak.
If each change hints three online devices on average, notifications can peak near 18,000/s. Notifications can be coalesced into "this namespace changed" instead of being reliable per-file messages. Hash-shard metadata by namespaceId, keeping ordered writes for one namespace on one primary shard. A very large shared space may become hot. Rate-limit bulk directory operations, coalesce notifications, and sub-shard file records by fileId only with evidence, while retaining a separate namespace sequence generator.
Step eight: close the failure, security, and validation loops.
- Interrupted upload: query the session and upload only missing parts; lifecycle processing expires abandoned sessions.
- Parts uploaded but commit failed: the parts are temporary orphans and manifest reconciliation reclaims them after a grace period.
- Commit succeeded but notification failed: the transactional outbox retries, and cursor pulls still recover.
- Commit succeeded but response was lost: the same idempotency key returns the original result.
- Concurrent offline writes: the
baseVersionIdcondition fails and the service preserves a conflict copy. - Corrupt chunk: upload and download verify a strong hash, and invalid content cannot enter a published manifest.
- Expired cursor: load a snapshot, then resume from its watermark.
- Home region cannot write: stop writes or fail over under an explicit RPO/RTO procedure; never allow two home writers.
Signed URLs must be short-lived and bound to an account, object, size, and operation. The server rechecks authorization and quota at commit. Global cross-user instant upload creates a side channel for content existence and couples per-user encryption and deletion rights, so deduplication is tenant-scoped by default. Core metrics include upload and commit p99, sync lag, expired cursors, conflict copies, orphan-chunk bytes, dedup hits, GC candidate-versus-deletion differences, hot namespaces, checksum failures, and recovery success.
Validation covers resumable transfer of a 50 GB file, crashes after every upload stage, duplicate commits, lost and reordered notifications, concurrent offline edits, an old device returning after a delete, checksum corruption, primary-shard failover, expired cursors, quota boundaries, and GC protection against false deletion. The most important end-to-end assertion is that every visible fileVersionId downloads to complete, verified content, and a version inside the recovery window is never reclaimed.
High-quality sample answer
"I would first separate the content plane from the metadata plane. Files are split into immutable chunks of about 4 MiB in object storage. Directories, stable fileId values, current versions, version manifests, tombstones, and namespace sequence numbers live in a metadata layer that supports conditional writes. Moves and renames update metadata without overwriting history.
During upload, the client hashes chunks and creates a session with baseVersionId and an idempotency key. The server returns short-lived signed URLs only for tenant-scoped missing chunks. After all chunks upload and verify, a metadata transaction confirms that the current version did not change, writes the new version and manifest, updates currentVersionId, and appends Change.seq plus an outbox record. Content completes before metadata publication, so a visible version never points to missing bytes. A lost response is recovered with the same idempotency key.
Synchronization uses a durable cursor. A notification only says a namespace may have changed. Devices call /changes?cursor=, apply changes in order, and then advance the cursor. Lost, duplicate, and reordered notifications therefore do not lose data. A cursor older than the 30-day retention window rebuilds from a consistent snapshot and resumes from its watermark. When two devices edit the same version offline, the later commit preserves a conflict copy instead of overwriting the newer version.
Capacity is 500 PB of logical storage. New or changed content is 400 TB per day, averaging about 4.6 GB/s and peaking around 25 GB/s. One hundred million versions average about 1,157 commits per second and peak near 6,000/s. Metadata is sharded by namespace with one home write region, while downloads scale through CDNs and regional object replicas.
A delete writes a tombstone retained for 30 days. When history expires, garbage collection marks candidates from all retained manifests, waits a grace period, and reconciles again. It never deletes solely because one reference count reached zero. I would inject failures at every upload and commit boundary, then verify lost notifications, offline conflicts, expired cursors, checksum corruption, primary failover, and GC safety while continuously asserting that every visible version downloads completely."
Common mistakes
- Store file bytes in the metadata database → large objects burden replication, backup, and transactions → store references in metadata and immutable content in object storage.
- Publish a file after the first part uploads → other devices can read an incomplete version → atomically commit metadata only after every part verifies.
- Treat WebSocket notifications as sync truth → a lost message or offline period permanently misses changes → use notifications only to trigger authoritative cursor pulls.
- Commit without
baseVersionId→ an offline device silently overwrites a newer version → use conditional commit and preserve a conflict copy on failure. - Generate a new session and idempotency key on every retry → duplicate versions, quota charges, and orphans grow → use a stable key to recover the original result.
- Default to global cross-user instant upload → content existence leaks and encryption/deletion become coupled → scope deduplication to an account or tenant.
- Delete chunks immediately after a user delete → offline sync, recovery, or delayed transactions reference missing bytes → use tombstones, retention, a grace period, and reconciliation.
- Assume a fixed deduplication ratio for physical capacity → an unknown workload produces false precision → report 500 PB logical and calibrate physical savings from measurements.
- Reliably push every file change to every device → notification cost and retry state explode → coalesce namespace hints and let clients pull deltas.
- Start with active-active cross-region metadata writes → name, move, and current-version conflicts become hard to converge → keep one home writer per namespace first.
Follow-up questions and responses
Follow-up one: How do you choose fixed-size versus content-defined chunks?
Chunks near 4 MiB are simple and predictable and work well for appends, localized overwrites, and most media files. If users frequently insert content near the beginning, fixed boundaries shift and every later hash changes. Content-defined chunking can rediscover unchanged content but uses more CPU and requires a stable, versioned boundary algorithm. Launch with fixed chunks, measure the percentage of reusable bytes after edits, and enable content-defined chunking for selected large files only when the observed savings justify the complexity.
Follow-up two: How do directory moves preserve sync ordering?
A move updates parentId and appends one namespace sequence in a metadata transaction. Clients apply it by seq. A move does not rewrite paths for every descendant because paths are derived from the parent chain. A cross-namespace move cannot pretend to be one local metadata update; model it as a retryable copy-and-delete workflow and expose an in-progress state to the user.
Follow-up three: How do end-to-end encryption and instant upload coexist?
With client-side end-to-end encryption, the server generally sees only ciphertext. Different per-user keys make identical plaintext produce different ciphertext, removing most cross-user deduplication. Convergent encryption introduces content-confirmation attacks and key risks. The product must choose: a high-privacy mode accepts lower deduplication, while a tenant-managed-key mode may deduplicate inside the tenant. It cannot promise unconditional global instant upload and strong end-to-end confidentiality at the same time.
Follow-up four: What if one enormous shared directory becomes hot?
First coalesce notifications, rate-limit bulk operations, cache read-only directory pages, and measure whether the bottleneck is sequence allocation, name uniqueness, or listing. File metadata may be sub-sharded by fileId, but the namespace still needs an ordered watermark. Internally, allocate sequence ranges in batches or use a partitioned log while exposing a stable external cursor. Do not discard recoverable ordering semantics merely to claim horizontal scale.
Follow-up five: How does garbage collection avoid false deletion?
GC does not trust live reference counts alone. It builds a live set from every manifest still inside retention, marks chunks outside the set as candidates, waits longer than the maximum transaction, replication, and recovery delay, and reconciles against current manifests before deleting. Deletes are idempotent and audited; missing manifests or mismatches delay reclamation. Unfinished multipart parts also need independent session expiration and abort processing.
Follow-up six: How do you provide cross-region disaster recovery?
Each namespace normally has one home region accepting metadata writes. Other regions asynchronously replicate its log and objects. Failover first fences the old writer, promotes a recovery replica under a new epoch, and changes routing. RPO depends on replication lag and RTO on detection and promotion. An RPO of zero requires a synchronous cross-region quorum and higher write latency. A recovered old primary must validate the namespace leadership epoch before accepting writes.
Follow-up seven: How do you prove that synchronization never misses data?
Build a state model that generates upload, commit, move, delete, conflict, and retry sequences. Assert that a client after applying sequence N equals the server snapshot at N. End-to-end tests randomly drop, duplicate, and reorder notifications without corrupting the change log; devices must still converge through cursor pulls. Inject crashes after applying a change but before saving the cursor, and after saving local content but before process completion. Replay must remain idempotent, and every device must ultimately reach the same version graph.