File Storage & Sync: Trade-offs & Decisions
Every architectural decision in this system has alternatives we chose not to take. This section explains each decision, the options considered, and why the chosen approach makes sense for this design's stated requirements and scale — while being explicit about when a different choice would be equally valid.
Proxy Upload vs Direct-to-Storage Upload
Should the application server receive file bytes and forward them, or should clients upload directly to object storage?
Proxy upload
Client → Application → Object Storage. The application server receives the entire file body and writes it to storage. Simple to implement, and the application can validate, transform, or inspect bytes in transit — this is a legitimate reason to choose proxying for specific requirements. Trade-off: it consumes application bandwidth and connection/resource capacity proportional to file size, couples data transfer to control-plane availability, and adds horizontal-scaling pressure as upload volume grows.
Direct upload via presigned URL
Client → Application (auth + presigned URL) → Client → Object Storage. The application only orchestrates: it authenticates, creates metadata, generates a presigned URL, and returns it. The client sends bytes directly to storage, which removes byte-proxying load from the application tier. Trade-off: presigned URL management adds complexity (expiration, scoping, security) — see Architecture Deep Dive → Presigned URL Security.
Database vs Object Storage for File Bytes
Where should the actual file bytes live?
Files in the database
Store file bytes as BLOBs alongside metadata. Can be a reasonable choice for small files in some systems — one transaction, strong consistency, no second system to coordinate. For this design's workload (large files, up to tens of GB, at petabyte-per-day ingest), relational databases are not well suited: multi-GB binary objects degrade query performance, bloat backups, and prevent independently scaling byte storage from metadata storage.
Files in object storage
Store bytes in a dedicated object store, purpose-built for large binaries with independent capacity scaling, a durability/cost model suited to bulk storage, direct/range access, and lifecycle policies. Trade-off: two systems to coordinate — metadata and bytes can become inconsistent if not handled carefully (see Architecture Deep Dive → Metadata/Object Consistency).
Chunk/Part Size Selection
What is the right chunk size for multipart uploads? There is no single cross-provider production range — it depends on several variables.
Small chunks
Fast retry on failure (little data to re-upload), but higher per-chunk coordination overhead — more presigned URLs, more HTTP round-trips, more part-state tracking. At very small sizes and very large files, coordination overhead can dominate.
Large chunks
Fewer chunks, less coordination overhead, but a failed chunk costs more to re-upload, and client memory/buffer pressure per chunk is higher.
Variables that actually determine the right size
Provider minimum/maximum part-size rules, maximum part-count ceiling, total file size, network conditions, client memory constraints, desired parallelism, and retry cost. Adaptive part sizing — adjusting chunk size based on observed connection quality — is a further optimization some clients implement.
Push vs Polling for Sync
How do devices discover that a file changed?
Polling
Each device periodically calls GET /sync/changes. Simple, stateless, works behind any firewall. Trade-off: latency is bounded by the poll interval, and a large device fleet polling frequently generates substantial request volume — most of it returning no new changes.
Push (WebSocket / long-poll)
The sync service maintains a persistent connection to online devices and pushes a change notification when something happens. This can reduce notification latency and wasted empty polling requests. Trade-off: requires infrastructure for persistent connections, reconnection logic, and fallback for devices that can't maintain them (restrictive firewalls, backgrounded mobile apps).
Hybrid approach (chosen)
Push for online devices as a low-latency hint; polling as a fallback and safety net. Both paths converge on the same durable, cursor-based change log as the source of truth.
Conflict Resolution Strategy
When two devices edit the same file offline, what should the system do?
Last-write-wins
Simple; the most recent write becomes current with no user intervention needed. Trade-off: it silently discards the other write. Whether that's acceptable is a product policy decision, not a distributed-systems requirement — 'first/last arrival becomes main' is a choice the product makes, not something the protocol mandates.
Conflict copy (preserve both)
Both versions are saved; the non-current one is exposed with a distinguishing name for the user to review and merge. Reduces the risk of silently overwriting one branch. Trade-off: requires user action, and ignored conflict copies can accumulate.
Three-way merge
Using the common ancestor version to auto-merge non-overlapping changes — works for some text formats, not for binary files, and is more complex to implement correctly.
SQL vs NoSQL for Metadata
What database technology should store file metadata?
SQL (e.g., PostgreSQL)
Transaction support well suited to permission changes and version-precondition checks, native JOINs useful for sharing queries, and mature tooling for schema evolution. Trade-off: horizontal write scaling requires deliberate sharding, which adds operational complexity.
NoSQL (e.g., DynamoDB, Cassandra)
Designed for horizontal write scaling and high throughput. Trade-off: typically no native JOINs, so sharing/relationship queries become application-level fan-out or require secondary indexes; consistency options vary by system and must be chosen deliberately for authorization-sensitive paths.
Version Retention Policy
How long should old file versions be kept?
Keep everything
Maximum recoverability — any version can be restored at any time, with a complete audit trail. Trade-off: storage cost grows without bound for frequently-edited files.
Time-based / count-based / quota-aware retention
Expire versions older than a window, or beyond a count, or once a per-owner storage quota is reached. Controllable storage cost. Trade-off: versions outside the retained window are not recoverable, unless a legal/compliance hold exception applies.
Whole-File Upload vs Delta Sync
Once a file changes, should the client transfer the entire file again or only the changed bytes?
Whole-file upload
Simplest to implement and reason about. The right default for small files, or any format where binary deltas provide little benefit (compressed/encrypted/binary-dense formats).
Fixed-block delta
Cuts transfer size for large files with in-place edits, but boundary shifts from insertions can defeat the savings.
Rolling/content-defined chunking
Finds matching regions despite shifted offsets, providing the best transfer savings for large files with localized edits — at the cost of CPU and chunk-index metadata.
Provider Multipart vs Application-Level Chunk Store
Should uploads use the storage provider's native multipart mechanism, or should the application maintain its own content-addressed chunk store?
Native multipart (chosen for baseline)
Simpler — the provider handles part storage and final object assembly. No additional application-level chunk metadata to maintain.
Application/content chunk store
Can enable cross-version chunk reuse, deduplication, and finer-grained delta semantics, but greatly increases metadata, reference-counting, and garbage-collection complexity — effectively building a mini content-addressed storage system on top of object storage.
Path-Based Identity vs Stable File ID
Should a file's identity be its path/name, or a stable identifier independent of both?
Path-based identity
Intuitive for simple systems, but rename/move become identity changes — any reference by path breaks, and reconstructing 'is this the same file after a rename' from path history is fragile.
Stable file ID (chosen)
file_id is assigned once and never changes; filename and parent_folder_id are just metadata fields that can be updated cheaply. This is what allows sync clients to recognize a rename as a metadata change on the same file, not a delete-plus-create.
Strong Metadata Mutation vs Eventual Sync Propagation
Does every part of this system need the same consistency model, or can different boundaries have different guarantees?
Committing a single file operation (e.g., 'accept this new version if the base version still matches') benefits from strong/conditional correctness at the metadata layer — this is where compare-and-set and version preconditions live. Propagating that committed change out to other devices, however, can be asynchronous and eventually consistent without weakening the core correctness guarantee, because each device reconciles against its own durable cursor rather than needing to observe the change instantly.
Owner Sharding vs File-ID/Tenant Sharding
How should metadata be partitioned as the system scales?
Owner/user sharding
Good locality for 'my files' queries. Weakness: a single very large or very active user/tenant can become a disproportionately hot or oversized shard, and 'shared with me' queries cross ownership boundaries.
Tenant sharding
Natural fit for multi-tenant products; same large-tenant hotspot risk as user sharding.
Hash/range by file_id
Better load distribution across shards, at the cost of weaker locality for 'list my files' queries, which then need a secondary index.
Direct Signed Download vs Application Proxy
Should downloads go directly from client to object storage/CDN, or through the application server?
Direct signed download (chosen for the general case)
Scales well — the application only issues a short-lived scoped URL after an authorization check; it does not carry byte-transfer load.
Application proxy download
Gives the application fine-grained real-time control — it can enforce immediate revocation, audit every byte served, or transform content on the way out. Trade-off: reintroduces the bandwidth/scaling cost that direct upload was designed to avoid.
Deduplication vs Simplicity/Isolation
Is content deduplication worth its complexity for this design?
Deduplication trades storage savings against: hashing CPU cost on every upload, reference-counting complexity for shared objects, potential privacy leakage if a 'does this content already exist' check is designed carelessly, weaker tenant isolation (shared underlying objects across owners), and meaningfully more complex garbage collection (an object can only be deleted once no owner references it).
Single Region vs Cross-Region Replication
Should the system operate in a single region, or replicate metadata and/or objects across regions?
Single region
Lower cost and operational complexity; metadata consistency reasoning stays simple. Trade-off: a regional outage affects the whole system, with recovery bounded by backup/restore rather than live failover.
Cross-region replication
Improves regional resilience and can tighten RPO/RTO, at meaningfully higher cost and operational complexity — replicated change logs, cross-region metadata consistency handling, and care to prevent split-brain writes during a partition.
Decision summary
| Decision | Default | Escalate when |
|---|---|---|
| Upload path | Direct-to-storage via presigned URLs | Proxy when inline inspection is mandatory, clients cannot reach storage, or a strict network boundary applies |
| File bytes | Object storage; relational DB for metadata | DB blobs only for small files where one transaction matters more than scale |
| Chunk size | Derived from provider limits, file size, network, memory and retry cost | There is no universal 5–10 MB rule; adapt to observed connection quality |
| Sync discovery | Push as a hint plus polling fallback over one cursor-based log | Polling alone when persistent connections are not possible |
| Conflicts | Conflict copy | Three-way merge for supported text formats; last-write-wins only as an explicit product policy |
| Metadata store | SQL (transactions, JOINs for sharing) | NoSQL when write scaling dominates, with consistency chosen deliberately for authorization paths |
| Version retention | Bounded, configurable policy with soft-delete grace | Keep everything, or legal holds, where recoverability outweighs cost |
| Transfer on edit | Whole-file for small or binary-dense files | Fixed-block or content-defined delta for large files with localized edits |
| Part storage | Native provider multipart | Application chunk store when delta or dedup savings justify the GC and ref-count complexity |
| File identity | Stable file ID | Path identity only for very simple systems without rename-safe sync |
| Consistency boundaries | Strong conditional commit; eventual propagation to devices | Do not treat sync propagation delay as a correctness bug |
| Metadata sharding | Chosen from tenant-size distribution and dominant query pattern | Hash by file_id for even load, with a secondary index for listing |
| Download path | Direct signed download | Proxy for immediate revocation, byte-level audit or inline transformation |
| Deduplication | Optional, later optimization | Global dedup only after a privacy-leak review |
| Regions | Single region with a bounded DR plan | Cross-region replication for specific availability or compliance requirements |