Dropbox / Cloud File Sync
Design a cloud file synchronization service that syncs files across multiple devices in real-time.
Low Level Design — covers service internals: DB schemas, API contracts, sequence flows, and class interfaces.
These designs are for learning purposes and may not be 100% accurate or production-ready. Always cross-reference with official docs and other resources. Report an issue if you spot something wrong.
Reference Documents
Dropbox — How We've Scaled Dropbox
Dropbox engineering talk on chunking, delta sync, block deduplication, and their sync engine architecture at scale.
Videorsync Algorithm — How Delta Sync Works
Original rsync technical report explaining rolling checksum and delta transfer — the foundation of Dropbox's sync algorithm.
ArticleContent-Defined Chunking (FastCDC)
USENIX ATC paper on FastCDC, a fast content-defined chunking algorithm used for variable-size chunk deduplication in storage systems.
ArticleLLD Overview
Low-Level Design — Dropbox File Sync
The core challenge is uploading and propagating only the changed bytes of a file across all of a user's devices. The LLD breaks into four sub-systems:
- ChunkUploadService — content-addressed storage for file blocks
- SyncStateManager — tracks per-device sync cursors and fans out change events
- Metadata DB — relational schema for files, versions, and chunk manifests
- Conflict Resolution — last-write-wins with copy-on-conflict fallback
Key Design Choices
| Concern | Decision |
|---|---|
| Chunk identity | SHA-256 of chunk content (content-addressing) |
| Deduplication | block_store table keyed by hash; identical chunks stored once |
| Delta sync | Client sends only hashes; server responds with missing set |
| Conflict detection | Vector clock on file_versions; divergence triggers fork |
| Fan-out | Kafka topic file.changed partitioned by user_id; one partition per device group |
Data Model
Database Schema
-- Core file identity (one row per logical file)
CREATE TABLE files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
path TEXT NOT NULL, -- relative path within user's Dropbox
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (user_id, path)
);
-- Immutable version history (one row per upload/edit)
CREATE TABLE file_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
file_id UUID NOT NULL REFERENCES files(id),
version_num INT NOT NULL,
size_bytes BIGINT NOT NULL,
vector_clock JSONB NOT NULL, -- { device_id: lamport_clock }
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (file_id, version_num)
);
-- Content-addressed chunk registry (global dedup)
CREATE TABLE block_store (
sha256 CHAR(64) PRIMARY KEY, -- hex SHA-256 of raw bytes
size_bytes INT NOT NULL,
s3_key TEXT NOT NULL, -- s3://bucket/{sha256}
ref_count INT NOT NULL DEFAULT 0, -- GC when 0
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Mapping: version → ordered list of chunks
CREATE TABLE chunks (
version_id UUID NOT NULL REFERENCES file_versions(id),
seq_num INT NOT NULL, -- chunk order within file
sha256 CHAR(64) NOT NULL REFERENCES block_store(sha256),
PRIMARY KEY (version_id, seq_num)
);
-- Conflict log: divergent versions that need user resolution
CREATE TABLE conflicts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
file_id UUID NOT NULL REFERENCES files(id),
version_a UUID NOT NULL REFERENCES file_versions(id),
version_b UUID NOT NULL REFERENCES file_versions(id),
resolved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Redis Keys
device:{device_id}:cursor → INT (last Kafka offset consumed)
chunk:upload:{upload_id} → HASH { total, received, chunks[] } TTL 24h
API Design
Internal Service APIs
ChunkUploadService
POST /v1/chunks/check
Body: { hashes: string[] } // client sends all chunk SHA-256s
Response: { missing: string[] } // server returns hashes it doesn't have
PUT /v1/chunks/{sha256}
Body: raw bytes (binary)
Response: 204 No Content
GET /v1/chunks/{sha256}/url
Response: { url: string, expires_at: string } // presigned S3 URL
SyncService
POST /v1/sync/commit
Body: {
file_id: string,
path: string,
device_id: string,
vector_clock: Record<string, number>,
chunks: Array<{ seq_num: number, sha256: string }>,
size_bytes: number
}
Response: { version_id: string, conflict?: ConflictInfo }
GET /v1/sync/delta?device_id={id}&since_cursor={cursor}
Response: {
changes: Array<{ file_id, path, version_id, operation: 'create'|'update'|'delete' }>,
next_cursor: number
}
SyncStateManager (internal gRPC)
service SyncState {
rpc GetDeviceState(DeviceStateReq) returns (DeviceState);
rpc UpdateCursor(UpdateCursorReq) returns (google.protobuf.Empty);
rpc BroadcastChange(ChangeEvent) returns (google.protobuf.Empty);
}
Sequence Diagrams
File Upload Flow
Conflict Resolution Flow
LLD Tradeoffs
LLD Tradeoff Analysis
| Decision | Option A | Option B | Chosen | Rationale |
|---|---|---|---|---|
| Chunk size | Fixed 4MB | Variable (rsync-style CDC) | Variable CDC in prod | CDC minimizes delta for small edits inside large files; fixed is simpler for MVP |
| Chunk identity | Random UUID | SHA-256 of content | SHA-256 | Enables deduplication across users; identical files share storage |
| Conflict strategy | Last-write-wins | Fork + conflict copy | Fork + conflict copy | Dropbox chose safety over simplicity; data loss is unacceptable |
| Fan-out mechanism | Polling | Push (Kafka → WebSocket) | Push | Reduces latency from minutes to seconds; polling wastes resources at scale |
| Device cursor storage | DB table | Redis | Redis | Sub-millisecond read for delta queries; DB would bottleneck at scale |
| Chunk upload auth | Pre-auth upload URL | Signed JWT | Presigned S3 URL | Offloads bandwidth from app servers; S3 handles multipart natively |
| Dedup scope | Per-user | Global | Global | ~30% storage savings from cross-user dedup (same PDFs, media, etc.) |
Core Classes
TypeScript Interfaces & Classes
// ─── Domain Types ───────────────────────────────────────────────
interface Chunk {
sha256: string; // hex SHA-256 of raw bytes
seqNum: number; // order within file (0-indexed)
sizeBytes: number;
}
interface FileVersion {
id: string;
fileId: string;
versionNum: number;
chunks: Chunk[];
vectorClock: Record<string, number>; // deviceId → lamport clock
sizeBytes: number;
createdAt: Date;
}
interface Delta {
missingHashes: string[];
chunkManifest: Chunk[];
vectorClock: Record<string, number>;
}
// ─── ChunkUploadService ─────────────────────────────────────────
class ChunkUploadService {
constructor(
private readonly s3: S3Client,
private readonly db: Pool,
) {}
/** Returns subset of hashes that are NOT in block_store */
async checkChunks(hashes: string[]): Promise<string[]> {
const rows = await this.db.query(
'SELECT sha256 FROM block_store WHERE sha256 = ANY($1)',
[hashes]
);
const present = new Set(rows.rows.map((r: any) => r.sha256));
return hashes.filter(h => !present.has(h));
}
async storeChunk(sha256: string, data: Buffer): Promise<void> {
const key = `chunks/${sha256}`;
await this.s3.putObject({ Bucket: BUCKET, Key: key, Body: data });
await this.db.query(
`INSERT INTO block_store (sha256, s3_key, size_bytes)
VALUES ($1, $2, $3) ON CONFLICT (sha256) DO NOTHING`,
[sha256, key, data.byteLength]
);
}
async getPresignedURL(sha256: string): Promise<string> {
return getSignedUrl(this.s3, new GetObjectCommand({
Bucket: BUCKET, Key: `chunks/${sha256}`
}), { expiresIn: 3600 });
}
}
// ─── SyncStateManager ───────────────────────────────────────────
class SyncStateManager {
constructor(
private readonly redis: Redis,
private readonly kafka: Producer,
) {}
async getDeviceCursor(deviceId: string): Promise<number> {
const val = await this.redis.get(`device:${deviceId}:cursor`);
return val ? parseInt(val) : 0;
}
async updateCursor(deviceId: string, offset: number): Promise<void> {
await this.redis.set(`device:${deviceId}:cursor`, offset, 'EX', 2592000);
}
async broadcastChange(event: {
userId: string; fileId: string; versionId: string; operation: 'create' | 'update' | 'delete';
}): Promise<void> {
await this.kafka.send({
topic: 'file.changed',
messages: [{ key: event.userId, value: JSON.stringify(event) }],
});
}
}
// ─── Client-side Delta Computer ─────────────────────────────────
class DeltaComputer {
/** Split file into ~4MB chunks using content-defined chunking */
computeChunks(buffer: Buffer): Chunk[] {
const CHUNK_SIZE = 4 * 1024 * 1024;
const chunks: Chunk[] = [];
let offset = 0, seq = 0;
while (offset < buffer.length) {
const slice = buffer.subarray(offset, offset + CHUNK_SIZE);
chunks.push({
sha256: sha256hex(slice),
seqNum: seq++,
sizeBytes: slice.length,
});
offset += CHUNK_SIZE;
}
return chunks;
}
}