FullStack Course LogoFullStack Course
Module: System Design
System Design·271·16 MIN READ

271: Object Storage, File Uploads, Media Processing, and Content Addressing

TOPICS COVERED: Object Storage, File Uploads, Media Processing, and Content Addressing

Learning outcomes

By the end of this lesson, you can:

  • explain and apply object stores in a realistic implementation;
  • explain and apply signed urls in a realistic implementation;
  • explain and apply multipart upload in a realistic implementation;
  • explain and apply metadata transaction in a realistic implementation;
  • explain and apply media pipeline in a realistic implementation.

These outcomes are connected. A reliable upload system is not just a file field and a database row: it is a sequence of decisions about where bytes travel, how ownership is enforced, how incomplete work is recovered, and how derived media becomes available. You should be able to explain both the happy path and the point at which each part of the system takes responsibility.

Prerequisites and retrieval

This lesson assumes the earlier 01–06 foundation and the preceding lessons in this module. Before reading, retrieve one concrete example from a previous project where the same concern appeared. Perhaps an application accepted images through its API, stored files on a local disk, generated thumbnails, or kept a URL in a database. Identify what happened to the bytes, what the database knew, and what would fail if the application ran on several instances.

The goal is not to memorize a list of storage-product terms. It is to make a defensible decision inside a large-scale distributed service. Requirements, traffic, failure modes, cost, and operational constraints need to be stated before a mechanism can be judged. Keep that earlier example in mind as a reference point while the design becomes more explicit.

Terminology

  • Object stores: Object storage addresses immutable-like blobs by key and scales capacity and durability independently from relational metadata. The key is the address of an object; it is not a promise that the store behaves like a local filesystem.
  • Signed URLs: The API can authorize an upload or download and then issue a short-lived signed URL, allowing the client to transfer bytes directly to or from storage. The signature should constrain the intended key, method, and other relevant conditions rather than acting as a general storage credential.
  • Multipart upload: Large files can be uploaded in parts, retried per part, and assembled when the upload is completed. This reduces the cost of restarting after a transient failure, although it creates incomplete upload state that must eventually be cleaned up.
  • Metadata transaction: Ownership, status, expected checksum, expected size and type, and processing state are stored in a database. The application finalizes the record only after the uploaded object has been verified, so a database row does not claim success merely because an upload was started.
  • Media pipeline: Upload events trigger virus scanning, transcoding, thumbnail generation, metadata extraction, and moderation asynchronously. Each job must cope with retries, and downstream consumers need a clear way to determine which output is current.
  • Content addressing: Hash-derived keys enable deduplication, integrity checks, and immutable caching. Access control still belongs to metadata and policy; knowing a hash must not automatically grant access to a private object.

The useful distinction is between the bytes and the facts about those bytes. Object storage is a good home for the blob. A database is usually a better home for ownership, lifecycle state, expected properties, and authorization policy. The two systems can fail independently, so the design must also describe how they are reconciled.

Mental model

Treat Object Storage, File Uploads, Media Processing, and Content Addressing as a design problem with observable inputs, outputs, invariants, and failure modes. Large binary objects should usually bypass application and database hot paths. Object storage, signed URLs, metadata, asynchronous processing, and CDN delivery separate the movement of bytes from transactional metadata.

A strong implementation makes its assumptions visible, narrows uncertainty at each boundary, and leaves enough evidence—tests, types, constraints, metrics, or diagrams—to show why the design is safe. For example, “the upload is complete” should have a precise meaning: the expected object exists, its size and checksum have been checked, and the metadata state has been advanced according to the service's rules.

A useful interview and production sequence is:

text
requirement -> constraints -> model -> implementation -> failure analysis -> verification

Do not jump from a requirement directly to a library call. First state what must remain true. Then choose the mechanism that enforces it. If a client can upload directly to storage, ask who authorizes the key and size. If a worker creates a thumbnail, ask what happens when the job runs twice. If a hash becomes part of a URL, ask whether that URL is public by policy or merely difficult to guess.

Deep dive

1. Object stores

When application instances have local disks, a file may appear to work on the instance that received it and disappear from the next instance that serves the user. Shared filesystem behavior, backup, durability, and capacity then become application-operational concerns. Object storage addresses immutable-like blobs by key and scales capacity and durability independently from relational metadata, which is why it is commonly used for uploads and derived media.

It is not a filesystem with arbitrary low-latency rename and append semantics. Treat an object as a completed value, and use explicit versioned or derived keys when a new value is produced. Metadata such as owner, visibility, lifecycle status, and processing state should not be inferred from the object store alone.

Decision rule: Use object stores deliberately when they make the contract or invariant easier to prove. If they only reduce typing while hiding an assumption—for example, who can read an object or how an abandoned object is deleted—prefer the more explicit design.

2. Signed URLs

Sending a large upload through an API server consumes server bandwidth, connection slots, and often temporary disk space. A signed URL lets the API make the authorization decision first, then lets the client transfer the bytes directly to storage. The URL is short-lived and should be scoped to the intended operation.

Limit the key, method, size and type expectations, and expiry. A URL for uploading one object should not become a reusable credential for arbitrary keys or downloads. The server still needs to verify what arrived; a client-provided content type or filename is an input, not proof of the object's actual contents. This is where people usually get confused: signing the request controls who may attempt an operation, but it does not replace metadata validation, malware scanning, or application authorization.

Decision rule: Use signed URLs deliberately when they make the contract or invariant easier to prove. If they only reduce typing while hiding an assumption, prefer the more explicit design. In particular, document whether the API authorizes the operation at URL creation time, at completion time, or both.

3. Multipart upload

For a large file, one failed request can otherwise force the client to start over. Multipart upload divides the file into parts, permits individual parts to be retried, and performs a final assembly operation. The retry boundary is smaller, so a transient network failure has a smaller cost.

The extra state is real. The service must track the upload identity, expected object, parts that have arrived, and whether completion has been requested. Completion should not be accepted for an incomplete or unexpected set of parts. Abandoned uploads need lifecycle cleanup, because an upload that never reaches the metadata-finalization step can still consume storage or provider-side resources.

Decision rule: Use multipart upload deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. Choose the part size, retry policy, completion behavior, and expiration policy as operational decisions, not as incidental SDK defaults.

4. Metadata transaction

The object and the database do not form one automatic transaction. A request can create a metadata row and fail before any object arrives; an object can arrive and the client can disappear before the API records completion. Store ownership, status, expected checksum, size and type, and processing state in a database, then finalize only after the uploaded object is verified.

A state such as pending, uploaded, processing, ready, or failed gives the API and workers something explicit to inspect. The exact state names are a design choice, but transitions must be constrained: a record should not become ready just because a client says the transfer finished. Verification should compare the observed object with the server's expectations, and authorization should be based on the owning metadata record rather than on an object key supplied by an untrusted client.

Decision rule: Use metadata transaction deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. The important trade-off is that consistency across two systems requires reconciliation, retries, and cleanup rather than a single pretend-atomic write.

5. Media pipeline

Media work is often too slow or too expensive to perform inside the upload request. Once an upload event is accepted, asynchronous jobs can perform virus scanning, transcoding, thumbnail generation, metadata extraction, and moderation. The user-facing API can report state while workers process the object independently.

Jobs must be idempotent. A queue may redeliver a message, a worker may crash after writing an output, or an operator may retry a failed job. Running the same job again should not corrupt the source or create an unbounded set of indistinguishable outputs. Use versioned or content-derived keys for outputs, record the processing version and result state, and make failures observable. A “ready” state should mean that the outputs required by the product are available, not merely that one worker finished.

Decision rule: Use a media pipeline deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. The asynchronous boundary improves request latency, but it also means clients must handle pending and failed states and the system must provide retry and monitoring behavior.

6. Content addressing

If the key is derived from a content hash, the same content can map to the same object key. That supports deduplication, integrity verification, and immutable caching: a key represents a particular byte sequence instead of a mutable filename. It can also make outputs easier to identify when a processing version or content-derived value is included in the key.

Content addressing does not answer the authorization question. A hash is not a permission check, and “hard to guess” is not the same as private. Keep visibility and ownership in metadata or policy, and require authorization before issuing a download URL or serving a private object. Hash collisions and the exact hashing and canonicalization rules also need to be considered when the hash is used as a security or identity boundary.

Decision rule: Use content addressing deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design. State whether the hash identifies raw bytes, a transformed representation, or a versioned processing result; those are different identities.

Worked example

Consider a large-scale distributed service whose requirements, traffic, failure modes, cost, and operational constraints must be made explicit. Start by writing the requirement in one sentence. Then list the input and output contracts and identify which concept above owns each failure mode.

For example, the requirement might be: “An authorized user can upload a video, and the service makes a verified, scanned, transcoded, thumbnail-ready version available through the CDN.” That sentence still leaves important questions: how large can the video be, which formats are accepted, when does the client see success, how long may processing remain pending, and what happens to an upload that is never completed? Those constraints determine the design.

The important move is separation. Parsing and basic validation belong at the boundary. Domain rules belong in the domain or service layer. Persistence rules belong in the database or repository. Presentation rules belong in the client. Mixing these concerns makes a happy-path demo look shorter, but it makes edge cases much harder to reason about and makes it unclear which layer can be trusted.

text
Client
  |
DNS -> CDN / Edge
  |
Load Balancer -> API instances -> Cache
                         |          |
                         +------> Primary datastore
                         |
                         +------> Queue / Stream -> Workers

In this model, the client may obtain a signed URL from the API and send the large body directly to object storage, even though the simplified diagram groups the API-facing path together. The primary datastore records ownership and state; the queue carries work; workers scan and transform the object; and the CDN serves approved, ready outputs. Each boundary needs an observable result, such as a stored state transition, a queue event, a worker result, or a metric.

Walk the example with at least four cases: the normal path, an empty or missing value, a duplicate, retry, or concurrent path where relevant, and a dependency failure. For the normal path, describe authorization, upload, verification, processing, and delivery. For a missing value, decide whether the API rejects the request or records an incomplete attempt. For a duplicate or retry, explain whether the operation is idempotent and what key or constraint prevents conflicting state. For a dependency failure, state whether the caller retries, sees a pending state, or receives an explicit error.

For each case, state which layer detects the problem and what the caller observes. This is the level of explanation expected in a senior code review or technical interview: not just the components, but the contract at each boundary and the evidence that the system reached the next state.

Production perspective

Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failure, stale clients, concurrent requests, malformed data, schema changes, and high cardinality. A worker deployed halfway through a processing-version change may see old messages; a stale client may retry a completion request after the record has already advanced; and a CDN may continue serving an older immutable output. These are design conditions, not unusual surprises.

Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Optimize only after you can identify the bottleneck or risk with evidence. For this topic, useful evidence includes upload and processing latency, bytes transferred, failed and abandoned multipart uploads, queue age, retry counts, storage growth, cache hit rate, and the number of objects without a matching metadata record.

When the topic involves an external dependency, define a timeout and cancellation strategy. When it involves persistence, define transaction and consistency expectations. When it involves user-visible state, define loading, empty, error, stale, and success states. When it involves security, assume the client can be modified and network input is untrusted. Never treat a filename, content type, checksum supplied only by the client, or content-derived key as authorization by itself.

Guided lab

Design a video upload service with direct multipart upload, metadata states, checksum verification, asynchronous transcoding, thumbnails, CDN delivery, deletion, and failed or abandoned upload cleanup.

Complete the lab with this discipline:

  1. Write the requirement and two non-requirements. For example, state what the service guarantees about supported input and availability, and explicitly state a concern it does not solve.
  2. List input, output, and error contracts before implementation. Include the states visible to the client and the conditions for moving between them.
  3. Implement the smallest correct vertical slice. It should connect the boundary, metadata, object, and one observable result before you add every transformation.
  4. Add at least one invalid-input test and one edge-case test. Include a case that would expose a mismatch between the client claim and the stored object.
  5. Instrument or inspect the behavior instead of guessing. Use logs, metrics, stored state, queue messages, or test output to establish what happened.
  6. Refactor one hidden assumption into an explicit type, constraint, function, or configuration.
  7. Explain one alternative design and why you did not choose it. Include the trade-off, not only the name of the alternative.
  8. Record a short “what would break at 10× scale?” note. Consider upload bandwidth, worker capacity, queue delay, storage lifecycle, database contention, and CDN behavior.

There is no single provider-specific implementation required by this lab. The design is successful only if it makes ownership, verification, state transitions, retries, derived outputs, deletion, and cleanup concrete enough to test and operate.

Edge cases and failure modes

  • Object stores: Test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Also check what happens when metadata points to a missing object or an object exists without a valid metadata record.
  • Signed URLs: Test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Check expired URLs, a URL used with the wrong method or key, and a client that changes the claimed type or size.
  • Multipart upload: Test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Check missing parts, repeated completion, interrupted uploads, late retries, and cleanup after expiration.
  • Metadata transaction: Test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Check an object arriving before the row is finalized, a repeated state transition, a concurrent delete, and a verification failure.
  • Media pipeline: Test absence, malformed input, duplicates, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Check redelivered jobs, worker crashes, unsupported media, partial output, stale processing versions, and a retry that encounters an output already written.

Common mistakes and debugging

  • Solving the example instead of the requirement: a copied pattern can be syntactically correct but architecturally wrong.
  • Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary” any values.
  • Testing only the happy path and therefore discovering contracts only after integration.
  • Optimizing before measuring, or selecting a scalable mechanism without a scale requirement.
  • Letting client-side behavior stand in for server-side authorization, validation, or persistence guarantees.

For debugging, reproduce the smallest failing case and record the identifiers that connect the systems: the metadata record, object key, upload identity, request, and job. Inspect the actual value or execution result, trace the boundary where the invariant first becomes false, and fix the owning layer rather than adding a downstream patch.

If the object is missing, inspect the signed request and storage event before changing worker code. If the object exists but the record is still pending, inspect the completion or verification path and its retry behavior. If processing is stuck, inspect queue age, worker logs, and the state transition rather than assuming transcoding is slow. If a private object is exposed, inspect authorization and URL issuance separately from the hash or CDN cache key. A normal result is not merely an HTTP success response; it is a consistent, observable state across the relevant boundaries.

Interview questions

  1. What problem do object stores solve, and what trade-off or failure mode would make you choose a different approach?
  2. What problem do signed URLs solve, and what trade-off or failure mode would make you choose a different approach?
  3. What problem does multipart upload solve, and what trade-off or failure mode would make you choose a different approach?
  4. What problem does a metadata transaction solve, and what trade-off or failure mode would make you choose a different approach?
  5. What problem does a media pipeline solve, and what trade-off or failure mode would make you choose a different approach?

Checkpoint

Without notes, explain Object Storage, File Uploads, Media Processing, and Content Addressing to another developer in five minutes. Your explanation must include one invariant, one edge case, one production failure mode, and one alternative design. Then implement a small example without copying the lesson code.

If you need a test for your explanation, ask whether another developer could identify where authorization happens, how an incomplete upload is represented, how a repeated job behaves, and why a content hash does not grant access to a private object. If those answers are vague, return to the relevant boundary rather than memorizing another product feature.

Mastery checklist

  • I can define the core terms precisely.
  • I can choose a design from requirements instead of from habit.
  • I can implement and test the normal path and edge cases.
  • I can explain the runtime, storage, or complexity cost.
  • I can identify which layer owns validation, errors, and recovery.
  • I can compare at least two reasonable alternatives.
  • I can explain how the design changes at larger scale or stricter reliability.

References

Reader page: /system-design/lesson/271/object-storage-file-uploads-media-processing-and-content-addressing