System design template

File storage service architecture.

The design goal is to keep file bytes away from your servers entirely, so that what you run is a metadata service that happens to be about files.

Download for macOS
v0.1.33 · .dmg · Apple Silicon & Intel
Signed & notarized by Apple · opens without a Gatekeeper warning
sha256 698955a0187bc039f4c74f5d05a9f10fbb27376a45788a0a241d1326b73873c7
Download for Windows instead
$curl -fsSL https://lucidtrain.com/install.sh | sh
File storage service architecture. 11 components across 4 tiers.
File storage service architecture. Rendered by the same ELK layout engine the app runs: the agent emits components, tiers and edges, and the engine places them, so the boxes cannot overlap.

The components

Every row below is read from the graph that produced the diagram above, so the two cannot disagree.

ComponentTierWhy it is there
ClientClientUploads direct to storage, not through the API
CDNEdgeSupporting component
API gatewayEdgeSupporting component
Metadata serviceApplicationSupporting component
URL signerApplicationPresigned PUT keeps bytes off our servers
ChunkerApplicationContent-defined chunks enable dedup
Virus scanApplicationSupporting component
Sync serviceApplicationDelta sync, not whole-file re-upload
PostgresDataSupporting component
Block storeDataSupporting component
KafkaDataSupporting component

Design decisions worth arguing about

A diagram shows what was chosen. It does not show what it cost, and that is usually the part that matters in a review or an interview.

Presigned uploads, so bytes never traverse your API

Proxying uploads means your API's bandwidth, memory and timeouts all scale with file size, and a few large uploads can starve every other request. Issuing a presigned URL and letting the client write directly to object storage removes that entirely. The cost is that you no longer observe the upload, so completion has to be confirmed by the client or by a storage event, and you must handle objects that were uploaded but never confirmed.

Content-defined chunking, not fixed-size blocks

Splitting at fixed offsets means inserting a byte near the start shifts every subsequent block and defeats deduplication completely. Choosing boundaries from the content itself keeps chunks stable across edits, so a small change re-uploads a small amount. It costs more CPU to compute boundaries and produces variable-size chunks, which complicates storage accounting.

Deduplication is a privacy decision as much as a storage one

Deduplicating across all users saves a great deal of storage and leaks information: upload timing reveals whether a chunk already existed, which tells you whether someone else has the same file. Deduplicating only within an account avoids that and saves much less. This is a decision that should be made deliberately, because it is very hard to reverse once the store is built.

Delta sync, because whole-file transfer does not scale with edits

Re-uploading an entire document because one paragraph changed is what makes sync feel slow on large files. Transferring only changed chunks makes edit cost proportional to the edit. It requires both ends to agree on chunk boundaries and to maintain an index of them, which is real complexity and the main reason simple sync tools do not do it.

How it changes with scale

Metadata operations vastly outnumber byte transfers, since listing, syncing and permission checks happen constantly while uploads are comparatively rare. That means the database is the component under pressure, not the storage layer, which is the opposite of what the name of the service suggests.

Where it breaks first

A sync loop. Two clients with slightly different views of the same file each write what they believe is the correct version, and each write triggers the other to sync again. Without version vectors and a conflict resolution rule that produces a stable outcome, this consumes bandwidth indefinitely and users watch a file change under them.

Draw this yourself

Open the Diagram tab and describe the system. The agent emits a semantic graph rather than coordinates, so you can edit the components and the layout re-solves instead of drifting.

shell
$ Diagram a file storage service: presigned direct uploads to object storage, a metadata service on Postgres, a chunker doing content-defined chunking for deduplication, virus scanning, a sync service doing delta sync, and CDN downloads.

When the shape is right, Implement in code turns the canvas into a markdown specification, every component, every relationship and the notes, and starts a real turn in the Code tab with it.

FAQ

Questions about this design

How should conflicts be resolved?

By keeping both versions and telling the user, in almost every case. Last write wins silently discards someone's work, and the moment it does that once, trust in the product is gone.

Where does virus scanning fit?

Asynchronously after upload, with the file quarantined until it passes. Scanning synchronously puts an unpredictable delay in the upload path and ties it to a scanner's availability.

How are permissions enforced on direct downloads?

By checking at the point the signed URL is issued and keeping the URL short-lived. The CDN and the storage layer never evaluate permissions themselves, so URL lifetime is the actual security boundary.

Do you need a database for file metadata?

Yes. Object storage can list keys but cannot efficiently answer questions about hierarchy, sharing or versions, and those are most of what the product does.

More templates