Designing a Cloud File Storage / Sync Service

A developer's guide to designing a Google Drive or Dropbox style service: how files move between a user's devices, how only the changed parts of a file travel over the wire, where bytes and metadata actually live, and how concurrent edits and offline clients are reconciled without losing work.

Student edition · start with the guarantee

Synchronize immutable file versions while keeping metadata publication, blob lifetime, and conflicts correct.

Correctness contract: A visible file version references durable authorized chunks. Latest-version changes use atomic base-version checks; active uploads/readers are protected from garbage collection.

Basic HTTP, databases, and programming are assumed. Specialized terms are explained below. Named products motivate the problem; this is an educational design, not a claim about their current internals. Capacity numbers are illustrative decimal payload estimates, not benchmarks. Add indexes, protocol overhead, replicas, headroom, and failure capacity before provisioning.

A cloud file storage service lets a user keep a folder of files in sync across every device they own, with the cloud as the authoritative copy. The surface looks simple — drop a file in a folder and it appears everywhere — but underneath there are two largely separate problems. One is storing bytes: taking possibly enormous files, breaking them into pieces, deduplicating and compressing them, and parking them durably in a blob store. The other is tracking truth: a metadata layer that records which files exist, who owns them, who they are shared with, what their revision history is, and which blocks make up each version. The interesting design work is in keeping those two layers consistent and in pushing changes out to other devices quickly. This guide walks through the requirements, then the upload path, the block and metadata layers, storage tiering, deduplication, sync and conflict handling, the notification path, and the consistency model that ties it together.

Contents

  1. Clarifying Questions
  2. Requirements
  3. Back-of-Envelope Estimation
  4. API Design and Data Model
  5. High-Level Architecture
  6. Upload Strategies
  7. Block Servers and Delta Sync
  8. Metadata and Caching
  9. Storage Tiering
  10. Deduplication
  11. Sync and Conflict Handling
  12. Notification Service
  13. Consistency Model
  14. Failure Modes
  15. Tradeoffs
  16. 45-Minute Interview Walkthrough
  17. Summary
  18. Common Mistakes
  19. Check Yourself
  20. Key Terms
  21. Further Reading
  22. Concepts to understand first
  23. Worked example
  24. Runnable Python model

Concepts to understand first

Each definition includes a concrete example and the condition under which the mechanism is useful. You can read this guide without opening another guide.

Resumable and multipart uploads

A persisted upload session tracks committed ranges or parts. After interruption, the client queries server progress and retries only missing bytes with checksums and an idempotent part identity.

Example. A 4 GB upload stops after three 100 MB parts. The server reports those durable parts, and the client resumes at the first missing part rather than trusting its last local send.

Limit. Validate ownership, total size, part checksums, and expiry. A completed blob becomes visible only after a conditional metadata commit and all referenced objects are durable.

Content-defined chunking

Splits a file at boundaries chosen from its bytes rather than fixed byte offsets, using a rolling/fingerprint scheme with minimum and maximum chunk sizes. Unchanged regions can recover the same boundaries after a local edit.

Example. Inserting bytes near a file's start shifts every later fixed-size boundary. Content-defined boundaries usually resynchronize, letting unchanged chunks keep their content hashes.

Limit. Savings are workload-dependent, not guaranteed one changed chunk. Enforce a maximum size, avoid empty trailing chunks, and measure bytes transferred rather than just changed chunk count.

Content addressing and garbage collection

An object is named by a cryptographic hash of its canonical bytes. Immutable versions reference object hashes. Garbage collection deletes only objects that no retained version or active upload can still use.

Example. Two file revisions reference one unchanged chunk. Deleting one revision must not delete that chunk while the other still references it.

Limit. Hash matches need an explicit collision model; cross-tenant existence checks can leak content. A grace period alone is insufficient for unbounded uploads: pin active sessions or bound their lifetime.

Optimistic concurrency and compare-and-set

Update state only if the expected version or status still matches, in the same atomic operation that changes it. A failed comparison means a conflicting operation won; reread or return a conflict.

Example. UPDATE seats SET status='held', version=version+1 WHERE id=? AND status='available' allows only one concurrent claimant to update the row.

Limit. Application-level GET then unconditional SET is not equivalent. Include acquisition identity to reject stale callbacks, and couple related invariants in a transaction.

Cache-aside and invalidation

A read checks a cache, reads the authoritative store on a miss, then fills the cache. A write commits to the store and invalidates or refreshes the cached value. TTL means time to live, not a freshness proof.

Example. A reader fetches version 1, a writer commits version 2 and invalidates, then the delayed reader caches version 1. DB-first invalidation alone does not prevent this refill race.

Limit. Use invalidation-aware fill leases or version fences for stronger behavior; a TTL only bounds one entry's residence under additional assumptions. Authorization and revocation need an explicit stale-data policy.

Transactional outbox

A business transaction writes its state change and an event row to the same database. A relay publishes committed event rows later, retrying until the broker durably accepts them.

Example. An order and its OrderCreated outbox row commit together. A relay crash after publish but before marking sent publishes the same event ID again on restart.

Limit. The relay is at-least-once, not magically exactly-once. Consumers dedup transactionally; preserve per-entity sequence when order matters. CDC is change data capture: reading committed database changes.

1. Clarifying Questions

Ask before you design. Each answer changes a box on the diagram.

Intuition

A file is like a book made of pages. The library keeps each unique page once, on a shelf labeled by its fingerprint. The catalog lists which pages, in which order, make each edition of each book. Syncing means sending only the new pages plus an updated catalog entry.

2. Requirements

Before drawing boxes it helps to pin down what the service actually promises. The functional requirements are modest in number but each one shapes the design.

RequirementWhat it means
Upload & downloadA user can add a file of any reasonable size and retrieve it later, byte for byte. Large files must survive flaky connections.
Sync across devicesA change made on one device propagates to the user's other devices automatically, ideally within seconds while they are online.
Revisions / historyPast versions of a file are retained so a user can view or restore an earlier state rather than only ever seeing the latest write.
SharingA file or folder can be shared with other users at a given permission level (view or edit), and their devices then sync it too.
Change notificationsWhen the cloud copy changes, online devices are told promptly so they can pull the update instead of polling blindly.

Uploaded versions must remain durable under a defined failure/retention model. Define separate guarantees for atomic version commits, read-your-writes synchronization, bounded-stale metadata reads, and fresh authorization. A stale immutable version can be coherent without being current; keep its chunks pinned while it is readable rather than calling this 'strong-ish'.

A useful framing: this is two systems wearing one coat. The block path is a content-addressed blob store optimized for throughput and storage cost. The metadata path is a relatively small, transactional database optimized for correctness. Keep them separate in your head and most of the design falls out.

3. Back-of-Envelope Estimation

Assumptions: 50M users with 10 GB each, 10M daily active users, 2 file uploads each per day, 500 KB average file, 30% saved by dedup, and 3 replicas. Peak is 2× average.

QuantityMathResult
Uploads per second10M × 2 / 86,400; ×2 at peak231 avg / 463 peak
Quota allocated50M × 10 GB500 PB (most never used)
New data per day20M × 500 KB10 TB
After dedup10 TB × 0.77 TB
Metadata50M users × 1,000 files × 1 KB50 TB
Long-poll connections5M online devices / 50k per server100 servers
Stored per day with 3 replicas7 TB × 321 TB/day ≈ 7.7 PB/yr
What the numbers tell you

Upload rate is modest. The hard parts are the metadata DB (50 TB, sharded, with heavy reads) and millions of open notification connections. Blob storage grows steadily and needs tiering.

4. API Design and Data Model

POST /v1/uploads                        # {path, size} -> {session_id}
PUT  /v1/uploads/{sid}?offset=N         # chunk body; returns committed offset
POST /v1/blocks/query                   # {hashes[]} -> which ones the server lacks
PUT  /v1/blocks/{hash}                  # upload one missing block
POST /v1/files/{id}/versions            # {base_version, block_hashes[]} -> new version or CONFLICT
GET  /v1/files/{id}?version=V           # metadata + ordered block list
GET  /v1/changes?cursor=C               # long poll; returns changes since cursor + new cursor
POST /v1/shares                         # {file_id, grantee, role}

The changes cursor matters. A device that was offline sends its last cursor and gets every change since. This is the durable form of the "offline backup queue".

TableKeyMain columns
filesfile_idowner_id, parent_id, name, is_folder, latest_version, deleted
versions(file_id, version)size, block_hashes[] (ordered), created_by_device, created_at
blockshashstorage_key, tier, size, last_access
shares(file_id, grantee)role (viewer | editor)
changes(namespace_id, seq)file_id, version, op; the per-user change journal

Shard metadata by owner or by shared namespace, so a folder tree and its change journal live on one shard. Then a commit is a single-shard transaction.

5. High-Level Architecture

A client talks to the service through a load balancer that fans requests out across a fleet of stateless API servers. Those servers own the metadata layer; the heavy lifting of file bytes is delegated to a separate tier of block servers writing to cloud storage. A notification service runs alongside to push changes back out to clients.

Cloud file storage architecture
A user's request enters through the load balancer and reaches an API server, which reads and writes the metadata DB through a metadata cache. Block servers handle file blocks and persist them to cloud storage, which tiers cold data down to cold storage. The notification service pushes changes to clients over long polling, with an offline backup queue holding updates for disconnected devices.

The components and their jobs:

6. Upload Strategies

Not all uploads are the same. A 20 KB note and a 4 GB video have completely different failure profiles, and the protocol should reflect that. The service supports two upload modes.

StrategyHow it worksBest for / tradeoff
Simple uploadThe whole file is sent in a single request. If it fails, the client retries the entire upload from the start.Small files. Minimal protocol overhead, but a dropped connection wastes everything already sent — unacceptable for large files.
Resumable uploadThe client first opens an upload session and receives a session ID. The file is then sent in chunks; each acknowledged chunk advances a committed offset stored against the session. After an interruption the client asks the server for the last committed offset and resumes from there.Large files and unreliable networks. Slightly more chatty, but a 30-second blip never costs more than the chunk in flight.

Resumable upload is what makes large-file sync feel robust. Because the session tracks progress on the server side, the client can crash, the laptop can sleep, the network can flap, and the upload still picks up where it left off rather than starting over.

# resumable upload: client side
function upload(file):
  if file.size <= SIMPLE_UPLOAD_LIMIT:
    POST /upload  body=file              # one shot, retry whole on fail
    return

  session = POST /uploads               # open session, get session_id
  offset = 0
  while offset < file.size:
    chunk = file.read(offset, CHUNK_SIZE)
    try:
      PUT /uploads/{session.id}?offset={offset}  body=chunk
      offset += len(chunk)
    except network_error:
      offset = GET /uploads/{session.id}/offset  # resume point
  POST /uploads/{session.id}/complete          # commit the file

7. Block Servers and Delta Sync

This is the central idea of the whole design. Files are not stored as single opaque blobs; each file is split into fixed-size blocks (a few megabytes each). The list of blocks, in order, is the file. Two things fall out of this that make sync efficient.

An in-place edit changes each block it overlaps. It may cross a boundary and affect multiple blocks; compressed/encrypted file formats can propagate a small logical edit into many changed bytes.

The second is the processing pipeline each block goes through before it lands in storage. On the way down, a block server takes a raw block and:

  1. Hashes it, producing a content fingerprint used for deduplication and as the block's identity.
  2. Compresses it, so the bytes stored and transferred are smaller.
  3. Encrypts it, so what lands in the blob store is unreadable without the key.
  4. Writes the resulting object to cloud storage, then records the block in the file→block mapping in metadata.

Download is the same steps in reverse: fetch the blocks for the requested version, decrypt, decompress, and reassemble them in order. Splitting files this way is what lets delta sync, deduplication, and tiering all operate at the granularity of a block rather than a whole file.

# client computes the delta, block servers process each block
function sync_changed_file(file, known_blocks):
  local_blocks = split_into_blocks(file)       # fixed-size or content-defined
  for block in local_blocks:
    h = hash(block)
    if h not in known_blocks:                  # only changed blocks
      payload = encrypt(compress(block))
      block_server.put(h, payload)             # write to blob store
  metadata.commit_version(file, [hash(b) for b in local_blocks])

Inserts break fixed-size blocks: content-defined chunking

Fixed-size blocks have a trap. Insert one byte near the start of a file and every later byte shifts right. Every block boundary now cuts different bytes, so every block after the edit gets a new hash. The "one small edit" re-uploads almost the whole file.

Content-defined boundaries usually resynchronize after local edits, allowing unchanged chunks to retain identities. The number/bytes of changed chunks is workload-dependent, not guaranteed one; bound minimum/maximum sizes and verify reconstruction.

Fixed-size: insert "X" into block 1 A B C DE F G HI J K LM N O P A X B CD E F GH I J KL M N OP all blocks new Content-defined: cuts follow the content A X B CD E F G HI JK L M N O P 1 chunk new Red = must upload. Green = hash already known, skip.
Fixed boundaries can shift after an insertion; content-defined boundaries often resynchronize. This diagram illustrates the benefit, not a universal one-changed-chunk theorem.

A small runnable demo. It inserts 12 bytes near the start of a 64 KB file and counts how many chunks must be uploaded again:

import hashlib, random

def fixed_chunks(data, size=1024):
    if type(size) is not int or size <= 0:
        raise ValueError("positive integer chunk size required")
    return [data[i:i+size] for i in range(0, len(data), size)]

def cdc_chunks(data, gear, mask=1023, minimum=256, maximum=4096):
    """Bounded educational gear-hash chunking, not a production CDC standard.
    Invariant: emitted nonempty chunks partition data[:start], each <= maximum.
    Bound: remaining input bytes. The last chunk may be shorter than minimum."""
    if len(gear) != 256 or minimum <= 0 or maximum < minimum or mask <= 0:
        raise ValueError("valid gear table and positive chunk bounds required")
    chunks, start, fingerprint = [], 0, 0
    for i, byte in enumerate(data):
        fingerprint = ((fingerprint << 1) + gear[byte]) & 0xffffffff
        size = i+1-start
        if size >= maximum or (size >= minimum and fingerprint & mask == 0):
            chunks.append(data[start:i+1])
            start, fingerprint = i+1, 0
    if start < len(data):
        chunks.append(data[start:])
    return chunks

def chunks():
    rng = random.Random(7)
    gear = [rng.getrandbits(32) for _ in range(256)]
    before = bytes(rng.getrandbits(8) for _ in range(64000))
    after = before[:1000] + b"hello, world" + before[1000:]
    for label, split in (("fixed", fixed_chunks), ("content-defined", lambda data: cdc_chunks(data, gear))):
        a, b = split(before), split(after)
        assert b"".join(a) == before and b"".join(b) == after
        assert all(0 < len(chunk) <= 4096 for chunk in b)
        known = {hashlib.sha256(chunk).digest() for chunk in a}
        changed = [chunk for chunk in b if hashlib.sha256(chunk).digest() not in known]
        print(label, "chunks", len(b), "; changed chunks", len(changed), "; changed bytes", sum(map(len, changed)))
    assert cdc_chunks(b"", gear) == []

chunks()

Output:

fixed 1 KB       chunks= 63  must upload=63
content-defined  chunks= 61  must upload=1

For this seeded 64 KB input, the bounded model reports 63 changed fixed-size chunks (64,012 bytes) versus one changed content-defined chunk (1,741 bytes). That is an observed example, not a general one-chunk guarantee.

Delta sync and the block hash work together: the client only sends a block whose hash the cloud has never seen. The hash both decides what to transfer (delta) and lets storage avoid keeping two copies of identical bytes (dedup, below).

8. Metadata and Caching

If the blocks are the bytes, the metadata is the meaning. The metadata DB records everything the byte layer does not understand:

Metadata reads vastly outnumber writes. Every directory listing, every sync check, every "has this changed?" poll touches metadata. To keep the database from being the bottleneck, hot metadata is served from a metadata cache sitting in front of it. A read first checks the cache; on a miss it falls back to the database and populates the cache. The catch is that a cache introduces the possibility of staleness, which is exactly the problem the consistency model (section 10) has to solve.

# read-through metadata cache
function get_metadata(key):
  value = cache.get(key)
  if value is not None:
    return value                  # cache hit
  value = metadata_db.get(key)    # miss: fall back to source of truth
  cache.set(key, value)
  return value

9. Storage Tiering

Most files are touched heavily right after they are created and then sit untouched for months or years. Treating every block as equally hot wastes money, because the fast, replicated storage that serves active files costs far more per byte than archival storage. The service therefore tiers blocks by access pattern.

TierHoldsProfile
Hot cloud storageBlocks for active, recently-accessed files.Fast reads, higher cost per byte. The default landing place for new blocks.
Cold storageBlocks not accessed for a long time.Much cheaper per byte, slower to retrieve. A background job migrates blocks here once they go cold.
Archive storageBlocks almost never read (old revisions, deleted-file retention).Cheapest. A read can take minutes to hours to restore (for example S3 Glacier Deep Archive, up to 12 hours on standard retrieval). Only safe for data a user will not open on demand, or with a "restoring" message in the UI.

A background process watches access timestamps and moves blocks that have not been read in a long while down to cold storage, updating their location in metadata. If a user later opens an archived file, the block is retrieved from the cold tier — slower, but rare, and the cost savings on the long tail of dormant data are large.

10. Deduplication

Content-addressed chunks can share bytes within an authorized dedup domain under a cryptographic collision model. Use conditional/idempotent object writes and immutable metadata references. Bare exists→put→refcount.set races and can lose references.

Garbage collection derives liveness from retained manifests and active session/read pins, using an epoch/grace or equivalent safe sweep protocol. Bound session lifetime; a grace period shorter than an active upload is not enough. Verify block ownership rather than letting a client reference any known hash. Cross-tenant existence checks and convergent encryption leak content information; a hash is not authorization or general anonymization. Tenant-scoped dedup is often safer.

11. Sync and Conflict Handling

Commit latest-version changes through an atomic compare-and-set transaction: verify base_version and permissions, insert the immutable version manifest, advance latest, and insert notification/outbox intent. A read of latest followed by unconditional add_version is racy: two devices can both pass the check.

On conflict, retain both edits, create a conflict copy, or invoke a format-specific reviewed merge. Block-level changes do not necessarily merge opaque formats semantically. Replayed commits use a stable operation identity; notifications can duplicate and devices catch up through a durable change cursor. Access/ownership changes are checked when committing and downloading.

12. Notification Service

Once a change is committed, the other devices need to learn about it without constantly asking. The service uses long polling: a device opens a request to the notification service that the server holds open instead of answering immediately. The connection stays parked until either a change relevant to that device occurs — at which point the server responds and the device pulls the update — or a timeout fires, after which the device simply re-opens the connection. This gives near-real-time delivery for online devices without the cost of a flood of empty polls.

Long polling only works for devices that are currently connected. A device that is offline — laptop closed, phone out of signal — cannot hold an open connection, yet it must not miss the changes that happened while it was away. That is the job of the offline backup queue: change events destined for a disconnected device are durably queued, and when the device reconnects it drains the queue and applies everything it missed before resuming normal long polling.

# notification fan-out on a committed change
function publish(file, recipients):
  for device in devices_of(recipients):
    if device.online:
      long_poll(device).respond(change_event(file))  # push now
    else:
      offline_queue(device).enqueue(change_event(file)) # deliver later

function on_reconnect(device):
  for event in offline_queue(device).drain():
    device.deliver(event)                # catch up on missed changes
  open_long_poll(device)                  # resume real-time path

13. Consistency Model

Metadata cache entries have explicit freshness semantics; TTL/invalidation do not by themselves guarantee latest committed state. A delayed reader can fetch old DB state, a writer commits and invalidates, then the reader refills old state. Versioned entries only help if a durable invalidation/version floor prevents a late fill after the key was deleted. Fill leases invalidate stale tokens; simpler TTL schemes provide only the stated weaker policy.

Base-version checks and authorization use the authoritative transaction, not an untrusted stale cache. Version manifests are immutable and chunks remain pinned while retained/readable. Stale metadata can remain coherent with its version without being current; preserve that distinction instead of promising 'strong-ish'. Revocation policy may require a fresh authorization check even when immutable bytes are cached.

14. Failure Modes

What failsEffectMitigation
Client crashes mid-uploadHalf a file sentResumable session. Blocks are only visible after the version commit, so no one sees a half file.
Blocks uploaded, commit failsOrphan blocksGarbage collector deletes unreferenced blocks after a grace period.
Two devices edit offlineConflict on commitBase-version check; conflicted copy; no edit is lost.
Notification server diesDevices miss pushesClients reconnect and send their cursor. The change journal replays what they missed.
Cache holds a stale entryOld file listingLeases or versioned sets, plus a TTL. Commits check the DB.
Metadata shard downThose users cannot syncReplicas with automatic failover; clients queue local changes.
Bug deletes blocksData lossGC grace period, soft deletes, and a dry-run mode that only logs.

15. Tradeoffs

DecisionOption AOption BUsual pick
ChunkingFixed-size: simple, fastContent-defined: survives insertsCDC for sync clients
Chunk sizeSmall (≈64 KB): better dedup, more metadataLarge (4 MB): less metadata, worse dedupAbout 1–4 MB average
Dedup scopeGlobal: most savingsPer user: better privacyPer user or per tenant
Push channelLong polling: works through any proxyWebSocket: lower overheadEither. Both need a cursor for catch-up.
Conflict policyLast writer wins: simpleConflict copy: never loses workConflict copy for opaque files

Worked example: two devices commit from version 7

Device A and B both edit base version 7.

Worked example, in orderUpload immutable chunks: Validate ownership/checksums and pin them through a bounded session. A blob existing by hash is not permission for another tenant to reference it.. A commits version 8: A transaction compares latest=7 and installs version 8 with its block manifest and notification outbox.. B tries the same base: Its compare-and-set fails. Preserve its bytes as a conflict version/copy or use an explicit merge policy; do not overwrite A through check-then-write.. Garbage-collect old data: Delete only unreferenced chunks after protecting retained versions, read snapshots, and active uploads. A fixed grace period alone fails if uploads/readers can outlive it.1Upload immutable chunks2A commits version 83B tries the same base4Garbage-collect old data
Read top to bottom. The table below explains the state and guarantee at each step.
StepState, calculation, and reasoning
1. Upload immutable chunksValidate ownership/checksums and pin them through a bounded session. A blob existing by hash is not permission for another tenant to reference it.
2. A commits version 8A transaction compares latest=7 and installs version 8 with its block manifest and notification outbox.
3. B tries the same baseIts compare-and-set fails. Preserve its bytes as a conflict version/copy or use an explicit merge policy; do not overwrite A through check-then-write.
4. Garbage-collect old dataDelete only unreferenced chunks after protecting retained versions, read snapshots, and active uploads. A fixed grace period alone fails if uploads/readers can outlive it.
Check your understanding: Does DB-first cache invalidation guarantee current metadata?

No. A delayed old DB read may refill after invalidation. Use invalidation-aware fill leases/version fences for stronger freshness, and enforce commit checks at the authority.

Run a small, testable model

Download concepts.py. It uses only the Python 3.10+ standard library. From the directory where you saved it, run:

python3 concepts.py chunks
python3 concepts.py test

The bounded chunking model reconstructs the exact input, avoids empty trailing chunks, and reports changed bytes as well as changed chunks. The version-CAS model is also available with python3 concepts.py versions.

The program contains deterministic assertions and failure traces. It is a learning model, not a production service: it does not implement network consensus, distributed durability, authentication, or third-party delivery. Read its contracts and try changing an input before extending it.

Primary references for these concepts

16. 45-Minute Interview Walkthrough

TimeDo this
0–5 minClarify: sync client, file sizes, sharing, revisions. State that real-time co-editing is out of scope.
5–10 minEstimate upload rate, storage per day, metadata size and connection count.
10–15 minAPI: upload session, block query, commit with base version, changes cursor. Tables.
15–25 minHigh-level design: API servers, metadata DB and cache, block servers, blob storage, notification service.
25–38 minDeep dives: chunking and delta sync (bring up CDC), dedup and GC, conflicts, cache consistency.
38–45 minFailure modes, tradeoffs, and extensions (search, previews, ransomware rollback).
Interview angle

A common follow-up is "the user inserts a line at the top of a 1 GB file. What gets uploaded?" With fixed blocks the honest answer is "almost everything". Then explain content-defined chunking. That answer shows real depth.

17. Summary

The design comes down to a handful of decisions that reinforce each other:

ConcernMechanism
How are large files uploaded reliably?Resumable uploads with an upload session and committed-offset tracking; simple one-shot upload for small files.
How is sync made cheap?Files are split into fixed-size blocks; only changed blocks are sent (delta sync), and they are compressed and encrypted before storage.
Where does truth live?A transactional metadata DB for files, users, versions, sharing, and the file→block mapping, fronted by a metadata cache for reads.
How is storage kept affordable?Hot cloud storage for active blocks, with a background job tiering cold blocks down to cheap cold storage.
How is duplicate data avoided?Content-addressed dedup: each block is identified by its hash and stored exactly once, reference-counted for safe deletion.
How are concurrent edits handled?Commits carry their base version; a stale base signals a conflict, resolved by last-writer-wins, conflict copies, or merge.
How do devices learn about changes?Long polling pushes changes to online devices; an offline backup queue holds events for disconnected devices until reconnect.
How is stale metadata avoided?Write the source of truth first, then invalidate or update the cache, so reads never serve metadata for a superseded version.
The recurring theme: separate the bytes from the meaning. A content-addressed block store handles bytes cheaply and durably through chunking, dedup, compression, and tiering; a small transactional metadata layer handles correctness — versions, sharing, conflicts — and a cache plus a notification path make it fast and live without letting any device drift onto a stale view.

18. Common Mistakes

19. Check Yourself

  1. A user adds one byte at the start of a 40 MB file split into 4 MB fixed blocks. How many blocks change?
    Answer

    All 10, plus a new small last block. Every byte after the insert shifts by one, so every block holds different bytes and gets a new hash. Content-defined chunking would change only about one chunk.

  2. Why does content-defined chunking use a minimum and a maximum chunk size?
    Answer

    The cut rule is random-like. Without limits, some chunks would be a few bytes (too much metadata) and some would be huge (bad dedup and large re-uploads). The limits keep chunks near the target size.

  3. Two uploads of the same new block run at once through store_block. What can go wrong?
    Answer

    Both see exists as false, both put the block, and both set the refcount to 1. The real count should be 2. Later one delete drops it to 0 and the block is removed while a file still uses it.

  4. Describe the stale-cache race with "write DB, then invalidate".
    Answer

    A reader misses and reads the old value from the DB. The writer then updates the DB and invalidates. The slow reader then sets the old value in the cache. It stays stale until a TTL or the next write. Leases or versioned sets block the late set.

  5. With 10M daily active users each uploading 2 files of 500 KB, 30% dedup and 3 replicas, how much storage is added per day?
    Answer

    20M × 500 KB = 10 TB. After dedup that is 7 TB. With 3 replicas it is 21 TB a day, about 7.7 PB a year.

  6. A laptop was offline for a week. How does it catch up without missing anything?
    Answer

    It sends the last cursor it saw to /changes. The server returns every journal entry after that cursor. The laptop pulls the new block lists and fetches only blocks it lacks.

20. Key Terms

Block / chunk
A piece of a file, stored and named by its hash.
Delta sync
Sending only the chunks that changed.
Content-defined chunking
Splits a file at boundaries chosen from its bytes rather than fixed byte offsets, using a rolling/fingerprint scheme with minimum and maximum chunk sizes. Unchanged regions can recover the same boundaries after a local edit. Example and limits above.
Rolling hash
A hash over a sliding window, cheap to update per byte.
Content addressing
Naming data by the hash of its bytes.
Mark-and-sweep GC
Mark live blocks, then delete the unmarked ones.
Convergent encryption
Encryption keyed by content hash, so dedup still works.
Lease (cache)
A token that makes a late cache fill fail after an invalidate.

21. Further Reading


⌂