all_lessons / system_design / cases / C15 C15 / C44

Design Google Drive

Drive looks like a file store, but the file bytes are the easy part. The product is a metadata system that keeps a tree consistent across devices that go offline for a week and come back disagreeing about reality. The hard problem is sync correctness, not storage.

Source: Vol. 1 Ch. 15 Case drill Metadata-first
First principle — the bytes are not the source of truth, the metadata is
A file on Drive is not "the bytes in object storage." A file exists, for any user, the instant a row appears in the metadata service pointing at a set of chunks — and not one moment sooner. Bytes can sit half-uploaded in the object store for hours; until metadata commits a version that references them, no client ever sees them. Hold this inversion in your head and the whole design falls out of it: upload is just shipping chunks to a dumb store; the interesting system is the metadata commit and the per-device sync that propagates it. Everything correctness-sensitive — dedup, conflicts, sharing, resumability — lives on the metadata side.

1. Clarify the contract

Treat the prompt as a product contract before a box diagram. Drive must:

And, just as important, what it need not do here. We are not designing the object store's durability internals (that is C16 — erasure coding, replication, scrubbing). We are not re-deriving resumable multipart-upload mechanics (offsets, ETags, part assembly) — that is anchored in C17; we link and assume it. Our job is the metadata tree, the commit protocol, and the sync log.

2. Put numbers on the shape

The headline number that forces the architecture is the cost of change, not the cost of bytes. Take a 4 GB file and a fixed chunk size of 4 MB:

chunks = 4 GB / 4 MB = 4096 MB / 4 MB = 1000 chunks (rounding 4 GB ≈ 4000 MB)

Each chunk is named by the hash of its content (e.g. SHA-256), not by its position. Now the user edits the file — say they touch a header and a footer that land in 2 of those 1000 chunks. Naive design re-uploads 4 GB. Content-addressed chunking re-uploads only the chunks whose hash changed:

re-upload = 2 chunks × 4 MB = 8 MB,  not 4 GB  (a 500× reduction)

That single arithmetic fact — dedup-by-content-hash turns a 4 GB re-upload into an 8 MB delta — is why every serious sync product chunks and hashes. It also gives cross-file and cross-user dedup for free: identical chunks (the same PDF mailed to 10 000 people) are stored once and merely referenced 10 000 times in metadata.

The second forcing number is on the sync side. A laptop is closed on a Friday and reopened the following Friday. In those 7 days the user's other devices and collaborators committed, say, 30 000 changes across their tree of 500 000 files. The reconnecting client must converge. Two ways to do that:

Re-scan whole tree
500,000 stat-equivalent ops
Replay delta log from cursor
30,000 change records
Edit-then-sync of a 4 GB file
8 MB on the wire
Chunks in that 4 GB file
1,000 @ 4 MB

A full tree diff is O(files) — half a million entries to walk and compare. Replaying a changes-since-cursor log is O(changes) — only the 30 000 things that actually moved. Both reach the same end state, but the log is the only one that stays cheap as trees grow. So the metadata service must expose, and durably retain, a per-client change feed. That is the spine of the design.

3. Define the surface area

Keep the API small and make it express the product operation, never the internal shard or queue layout. The commit step takes a precondition — that is what makes concurrent edits safe.

API / operationWhy it exists
POST /upload-session → session idOpens a resumable upload; client streams chunks against it (mechanics deferred to C17).
PUT /chunk {session, hash, bytes}Stores one content-addressed chunk; idempotent — re-PUT of a known hash is a no-op (dedup).
POST /commit {fileId, baseVersion, chunkList, idemKey}Atomically creates a new version iff the file is still at baseVersion. The correctness fulcrum.
GET /folder/{id}Lists a folder's current metadata.
GET /changes?cursor=…Returns changes since the cursor + a new cursor. The sync engine's heartbeat.
POST /share {resourceId, principal, role}Grants access; checked on every metadata read and download-URL mint.

4. Model the data

The data model is the first architecture. Split it cleanly along the first-principle boundary: a metadata service (the source of truth — small rows, transactional, the thing we replicate for consistency) and a chunk/object store (huge, dumb, content-addressed, immutable blobs).

file (id, version, owner)folder tree (parent edges)version → chunk-hash listchunk (hash → bytes, refcount)permission (resource, principal, role)change-log (namespace, seq, op)

The pivotal table is version → ordered list of chunk hashes. A file version is nothing but a manifest of hashes; the bytes live once in the chunk store keyed by hash. Editing a file means writing a new manifest that reuses most of the old hashes and adds a few new ones — which is exactly why the 8 MB delta above works.

5. Linearized design

Walk an upload-then-sync through the system in the order it actually happens; the bottleneck (the commit) shows up on its own.

  1. 1. Client splits the file into 4 MB chunks and hashes each. It asks the metadata service which hashes are already known and uploads only the unknown ones to the chunk store (dedup happens before any byte moves).
  2. 2. Chunks land in object storage as immutable, content-addressed blobs. At this point the file still does not exist for any user.
  3. 3. Client calls commit with the chunk-hash manifest, the baseVersion it edited from, and an idempotency key. The metadata service runs a single transaction: check the precondition, write the new version row, bump refcounts, append a record to the change log.
  4. 4. The commit's change record is appended to the owning namespace's sync log with a monotonic sequence number — this is the durable, replayable feed.
  5. 5. Every other device tails GET /changes?cursor, sees the new sequence number, learns the new manifest, and pulls only the chunk hashes it lacks.
  6. 6. On download, the metadata service checks permissions first, then mints a short-lived signed URL straight to the chunk store so bytes never transit the metadata tier.
THE TWO TIERS (source of truth vs. dumb bytes) device A ─┐ ┌──────────────────────────────┐ device B ─┼── commit ────▶ │ METADATA SERVICE │ refcounts, device C ─┘ (manifest + │ (transactional, replicated)│ version rows, base version) │ ┌────────────────────────┐ │ permissions │ │ file → version → [hashes]│ │ │ │ precondition commit │ │ │ └──────────┬─────────────┘ │ │ │ append │ │ ┌────────▼──────────┐ │ │ │ SYNC LOG (seq) │ │ │ │ …,1041,1042,1043 │ │ │ └────────┬──────────┘ │ └──────────────┼────────────────┘ per-device cursor ───────────────┘ "give me changes > my seq" device A cursor=1043 device B cursor=1041 (2 behind) device C cursor=0998 (offline 7d) signed URL ▼ (only after permission check) ┌───────────────────────────────────────────────────────────────────┐ │ CHUNK / OBJECT STORE — immutable, content-addressed, deduped │ │ hash:a1b2 → 4MB hash:c3d4 → 4MB … (one copy, many references) │ └───────────────────────────────────────────────────────────────────┘

6. Deep dives

(1) Metadata is the source of truth — uncommitted chunks never leak

The most common junior mistake is to treat "bytes are in S3" as "the file is saved." It isn't. The object store is allowed to be full of orphaned, half-uploaded, garbage chunks at any moment — a client that dies mid-upload leaves them there. The system stays correct because visibility is gated entirely by the metadata commit: a file version is real only when its manifest row exists, and that row is written in a single transaction. There is no intermediate state where a user sees a file pointing at chunks that aren't all present, because the manifest is written after the chunks are confirmed and atomically.

This is DDIA Ch. 9's linearizability argument applied to the file tree: every reader must agree on whether version N exists, and a single linearizable commit point gives that. Orphaned chunks are reclaimed lazily by a refcount sweep (a chunk with refcount 0 across all manifests for some grace period is deleted) — they cost storage, never correctness. The contrast with the object store is deliberate: the chunk store can be eventually-consistent and durability is its own problem (C16); only the metadata tier pays for strong consistency, and it is small, so it can afford to.

(2) The sync log — changes-since-cursor, not a tree scan, and it must survive absence

From §2 we know a reconnecting client must replay O(changes), not diff O(files). The mechanism is a per-namespace append-only log of metadata mutations with monotonic sequence numbers, and a per-client cursor that records "the last seq I have applied." Sync is then trivially: send my cursor, receive every record after it, apply them, advance the cursor. This is precisely DDIA Ch. 11's change-data-capture / event log: the metadata table is the state, the log is its ordered stream of changes, and clients are independent consumers reading at their own offsets — exactly like Kafka consumer groups.

The non-obvious requirement is the log must survive the client's absence. A device offline for 7 days expects its cursor (998 in the diagram) to still be a valid anchor when it returns. So the log can't be a transient in-memory pub/sub that forgets old entries — it must be durably retained, at least back to the oldest live cursor. To bound that retention, the system compacts: when entries fall before the oldest cursor, or when a single file has churned 500 times, old per-version records collapse into a single "current state" checkpoint. A client too far behind to be served from the log falls back to a full resync (a one-time tree snapshot) and then resumes tailing — degraded but always correct.

CONFLICT: two offline edits, one base version (DDIA Ch.5 "syncing") file f, base version = v7 [chunks: h1 h2 h3 ...] │ ┌────┴───────────────┐ ┌────────────────────┐ │ device A (offline) │ │ device B (offline) │ │ edits → manifest M_A │ │ edits → manifest M_B │ └────┬───────────────┘ └────────┬───────────┘ │ reconnect first │ reconnect second ▼ ▼ commit{base=v7, M_A} commit{base=v7, M_B} │ │ server: current==v7 ✓ server: current==v8 ✗ (≠ base v7!) → write v8, log seq++ → PRECONDITION FAILED │ │ ▼ ▼ A wins the line B's edit is NOT discarded: create "f (conflicted copy — device B)" as a sibling version. User decides.

(3) Conflict resolution — version-precondition commit, loser becomes a conflicted copy

Now the sharpest case. Two devices were both offline, both edited the file that was at version 7, and now both reconnect. If commit were a blind "write my manifest," the second writer would silently clobber the first — data loss, the cardinal sin of a sync product. The fix is the compare-and-set / version-precondition commit in the API: commit carries the baseVersion the client edited from and succeeds only if the server's current version still equals it. This is DDIA Ch. 9's fencing-by-version and Ch. 7's optimistic concurrency control: the version number is a token that detects the lost-update race.

Device A reconnects first, server is at v7, precondition holds, A's edit becomes v8, a record is logged. Device B reconnects, presents base=v7, but the server is now at v8 — precondition fails. Crucially, B's edit is not thrown away: the bytes are already deduped in the chunk store, so the system materialises B's manifest as a conflicted copy — a sibling file ("f (conflicted copy — device B)") — and lets the human resolve it. This is exactly DDIA Ch. 5's replication-conflict discussion, where the "two clients editing a calendar entry while offline" example resolves to keeping both versions rather than silently picking one. The idempotency key on commit (DDIA Ch. 12) handles the orthogonal problem of retries: if A's commit response is lost and A retries, the key makes the second attempt a no-op returning the same v8, so a network blip never creates a phantom duplicate version.

7. Trade-offs

ChoiceBuysCostsChoose when
Content-hash chunk dedup vs. whole-file blobs8 MB deltas not 4 GB; cross-user storage savings; resumability1000× more metadata rows; chunk-existence probe can leak "this file already exists" (privacy)Large files that get edited or are widely shared (the Drive default)
Strong folder transactions vs. eventually-consistent treeAtomic moves/renames; no orphaned subtreesMetadata write bottleneck; harder to shard a hot shared folderCollaborative folders where users see each other's moves
Push sync (long-poll / streaming) vs. poll syncNear-instant propagation; lower latencyServer holds connection state per device; reconnection stormsDesktop/mobile clients that stay online
Version retention forever vs. policy (N versions / 30 days)Full recovery; audit trailUnbounded storage and refcount growthEnterprise/compliance plans; cap on consumer tiers

The sharpest of these is chunk dedup vs. whole-file blobs, because the win (500× on the wire, single-copy storage of viral files) is so large it feels free — but it isn't. Each file now costs ~1000 metadata rows instead of one, so the metadata tier you must keep strongly consistent grows three orders of magnitude. And dedup leaks a side channel: if the upload protocol skips a chunk because the server "already has that hash," a probing client learns that someone already uploaded that exact content — a real privacy/security concern that forces per-user dedup scoping or hashing tricks. You accept dedup because edit-and-share is Drive's whole point; you pay for it in metadata volume and a careful upload protocol.

8. Failure modes

FailureMitigation
Concurrent commit race (two offline edits)Version-precondition commit; loser becomes a conflicted copy, never a silent overwrite (deep dive 3).
Duplicate commit from a retryIdempotency key on commit (DDIA Ch. 12) — second attempt returns the same version, no phantom duplicate.
Client offline for days, cursor staleDurable, compacted sync log retains back to the oldest live cursor; too-far-behind clients fall back to full resync then resume tailing.
Orphaned / half-uploaded chunksVisibility gated by metadata commit; refcount-0 chunks reclaimed by a lazy sweep — costs storage, never correctness.
Permission leak via cached signed URLShort-TTL signed URLs minted only after a permission check; revoke-sensitive files re-check on each download.
Hot shared folder / huge flat folderPartition metadata by namespace; cap files-per-folder; serialise moves through the folder's owning shard.

9. Interview Q&A

  1. Two devices commit different edits to the same file while one was offline — what does the user see? (senior answer) Both commits carry the baseVersion they edited from. The first to reconnect finds the server still at that base, so its precondition holds and it becomes the new version. The second presents the now-stale base, its precondition fails, and instead of clobbering the winner the server materialises its manifest as a conflicted copy — a sibling file the user resolves. Nothing is silently lost; that's the whole point of version-precondition commit (DDIA Ch. 5/9).
  2. Where is the source of truth — the bytes or the metadata? (senior answer) The metadata. A file exists for users only after the metadata commit writes its version manifest; until then chunks in the object store are invisible orphans. This lets the chunk store be eventually-consistent and dumb while only the small metadata tier pays for linearizability (Ch. 9).
  3. A laptop reconnects after a week — how does it catch up without re-scanning everything? (senior answer) It sends its saved cursor to GET /changes and replays only the records after it — O(changes), e.g. 30k records, not O(files) half a million stats. The sync log is durable CDC (Ch. 11) retained back to the oldest live cursor; a client too far behind falls back to a one-time full resync.
  4. User edits 8 MB of a 4 GB file — how much goes over the wire? (senior answer) ~8 MB. At 4 MB chunks the file is 1000 content-addressed chunks; only the 2 chunks whose hash changed are re-uploaded, the manifest reuses the other 998 hashes. That 500× reduction is the entire economic reason to chunk-and-hash.
  5. What happens to the chunks of a file you never finished uploading? (senior answer) They sit in the object store as orphans referenced by no manifest, so no user ever sees them. A refcount sweep reclaims chunks at refcount 0 after a grace period. They cost storage, never correctness — because visibility is gated by the metadata commit, not by byte presence.
  6. How do you keep dedup from leaking who-has-what? (senior answer) A naive "skip upload, server already has this hash" tells a prober that someone owns that exact content. Mitigate by scoping dedup per-user (no cross-tenant probe) for sensitive data, or by requiring proof-of-possession of the full chunk before crediting a skip. You trade some global dedup savings for closing the side channel.
  7. Why not just diff the file trees on every sync? (senior answer) A tree diff is O(files) per sync and rescans unchanged data forever; it doesn't scale to half-million-file accounts syncing every few seconds. The change log is O(actual changes) and lets each device read at its own durable offset — the same consumer-offset model as a partitioned log (Ch. 11).
  8. How is a commit made safe against retries? (senior answer) An idempotency key (Ch. 12, foundation lesson 12). If the commit succeeds but the response is lost and the client retries, the key dedups it server-side and returns the same version — so a flaky network can't create a duplicate version alongside the conflict path.

Related lessons

This case deliberately defers two things rather than re-deriving them. Resumable multipart upload — session offsets, part assembly, retry of a single failed part — is anchored in C17; we assume it and focus on what happens at commit. Object-store durability — replication, erasure coding, anti-entropy scrubbing of the chunk store — is C16's problem; here the chunk store is a dumb content-addressed blob bucket. The commit protocol leans on foundation lesson 12 (idempotency & transactions) for the idempotency key and on lesson 09 (consistency & CAP) for why only the metadata tier needs linearizability while the chunk store can stay eventually-consistent.

C17 — upload/chunk anchor C16 — object storage durability 12 — idempotency & transactions 09 — consistency 13 — fault tolerance
Takeaway
Drive is a metadata system that happens to point at bytes. Make the metadata commit the single source of truth and uncommitted chunks can never leak. Chunk by content hash and a 4 GB edit ships as 8 MB, not 4 GB. Sync by a durable change-log cursor — O(changes), not O(files) — so a device offline for a week catches up by replay, not rescan. And make commit a version-precondition compare-and-set so two offline edits resolve to a winner plus a conflicted copy, never a silent overwrite. Defer the byte mechanics to C16/C17; the correctness lives in the metadata.