System Design Article
Blob Storage, Object Stores & CDNs
Difficulty: Medium
Databases are wrong for storing large unstructured files - photos, videos, backups, logs. Object stores like S3 give you cheap, durable, infinitely scalable storage for blobs, while CDNs cache that content at edges close to users. This lesson covers the object-storage data model, multi-part upload, storage classes, presigned URLs, and how a CDN turns a globally slow origin into a globally fast experience. By the end you can design the media layer for any social, video, or e-commerce system.
Blob Storage, Object Stores & CDNs
Databases are wrong for storing large unstructured files - photos, videos, backups, logs. Object stores like S3 give you cheap, durable, infinitely scalable storage for blobs, while CDNs cache that content at edges close to users. This lesson covers the object-storage data model, multi-part upload, storage classes, presigned URLs, and how a CDN turns a globally slow origin into a globally fast experience. By the end you can design the media layer for any social, video, or e-commerce system.
698 views
20
Block, File, and Object Storage
Before we talk about object stores, anchor the three storage paradigms in mind.
| Type | Interface | Example | Best For |
|---|---|---|---|
| Block | Read/write fixed-size blocks at offsets | EBS, local SSD | Databases, OS volumes |
| File | Hierarchical paths, POSIX semantics | EFS, NFS, SMB | Shared application file systems |
| Object | HTTP API, flat namespace, immutable objects | S3, GCS, Azure Blob | Large unstructured blobs at internet scale |
Object storage wins for blobs because the API is dead simple (HTTP), there is no coordination cost between clients, and the underlying system can scale storage and bandwidth independently across thousands of nodes.
The Object-Storage Data Model
An object store has three concepts:
- Bucket: a globally named namespace (
my-app-uploads). - Key: an arbitrary string that identifies an object within a bucket (
avatars/users/42/profile.webp). Keys look like file paths but the namespace is flat. - Object: the binary body plus user-defined metadata (key/value strings). Once written, objects are immutable - to 'update' an object, you write a new version (or overwrite the same key, breaking caches).
---------- S3 object example ----------
bucket = my-app-uploads
key = videos/abc/HD/segment_0001.ts
body = <2.4 MB binary>
meta = { content-type: video/mp2t,
cache-control: public, max-age=31536000 }
version = 'k8Hp2...' (only if versioning is enabled)Durability and Availability
Object stores like S3 advertise eleven nines (99.999999999%) of durability. They achieve this by replicating each object across multiple devices and availability zones - typically using erasure coding (a fancier form of RAID) so objects survive multiple disk and AZ failures without storing 3x raw copies. Availability is lower (99.99% for S3 Standard) - the data is there, but the API may briefly be unreachable.
The consequence: never store the only copy of important data only in your database. Heavy assets (photos, videos, exports) belong in an object store, with the database holding metadata and pointers.
Multi-Part Upload
Large uploads cannot be done in a single HTTP request. The standard pattern:
- Client tells the server 'I want to upload a 5 GB video'.
- Server initiates a multi-part upload (
CreateMultipartUpload) and returns an upload ID. - Client splits the file into N parts (5 MB to 5 GB each), uploads them in parallel directly to the object store using presigned URLs generated by the server.
- Client tells the server 'I'm done, here are the part ETags'.
- Server calls
CompleteMultipartUploadto atomically assemble the object.
Why this matters:
- Resumable: if part 7 fails, only retry part 7.
- Parallel: saturate the network link with many concurrent part uploads.
- No proxying: the bytes never go through your server, which would otherwise be an instant bandwidth bottleneck.
Presigned URLs
A presigned URL is a normal https://... URL that contains a signed query parameter authorizing a specific operation (PUT, GET) on a specific object for a limited time. The server generates it; the client uses it directly against the object store.
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: 'us-east-1' });
async function getUploadUrl(userId, filename) {
const cmd = new PutObjectCommand({
Bucket: 'my-app-uploads',
Key: `avatars/users/${userId}/${filename}`,
ContentType: 'image/webp'
});
return await getSignedUrl(s3, cmd, { expiresIn: 900 }); // 15 minutes
}The client then uploads with a plain PUT to the returned URL. The server sees no bytes; it only orchestrates.
Storage Classes and Lifecycle Rules
S3 (and equivalent services) offer multiple storage classes that trade access latency for cost.
| Class | Latency | Cost (relative) | Use Case |
|---|---|---|---|
| S3 Standard | ms | 1x | Hot user-facing data |
| Intelligent-Tiering | ms | 0.4x-1x (auto) | Unknown access patterns |
| Standard-IA | ms | 0.5x | Backups, infrequent reads |
| One Zone-IA | ms | 0.4x | Recreatable content (thumbnails) |
| Glacier Instant Retrieval | ms | 0.2x | Archives accessed once a quarter |
| Glacier Flexible Retrieval | minutes-hours | 0.05x | Audit logs, regulatory backups |
| Glacier Deep Archive | 12+ hours | 0.02x | Compliance archives |
Lifecycle policies automatically move objects between classes based on age. A typical rule for user uploads:
# S3 lifecycle config
rules:
- id: archive-old-uploads
filter: { prefix: uploads/ }
transitions:
- days: 30
storageClass: STANDARD_IA
- days: 180
storageClass: GLACIER
expiration: { days: 2555 } # 7 years for complianceThis single config can cut a media bucket's monthly cost by 70-80% over time.
What a CDN Actually Does
A Content Delivery Network is a global fleet of caching proxies. When a user requests https://cdn.example.com/avatar.webp:
- DNS routes them to the nearest edge location (Anycast or geo-DNS).
- The edge checks its cache for the object using a cache key (typically the URL plus selected request headers).
- Cache hit -> serve from edge. Latency is the round trip to the nearest edge - usually 5-30 ms.
- Cache miss -> fetch from the origin (your S3 bucket or app server), serve the response, and store it in the edge cache for next time.
---------- Pull-through CDN ----------
edge cache (Tokyo)
^ miss
user (Tokyo) -->|--> origin (us-east-1, S3)
v hit (next request)
edge cache (Tokyo) -- 30 ms RTTThe wins are huge:
- Latency: serving from a city near the user beats one trans-Pacific round trip.
- Bandwidth offload: the origin sees a tiny fraction of total traffic.
- DDoS absorption: the edge fleet has more capacity than any one origin.
- Encryption termination: TLS is handled at the edge.
Pull vs Push CDNs
- Pull: the CDN fetches from origin on first miss. Default for S3 + CloudFront, Cloudflare, Fastly. Almost always the right choice.
- Push: you upload assets to the CDN explicitly. Used for time-sensitive launches when you want to pre-warm caches.
Cache Control: TTL, Cache-Control, ETag
The origin tells the CDN (and the browser) how to cache via HTTP response headers:
---------- Common cache headers ----------
Cache-Control: public, max-age=31536000, immutable -- versioned static assets
Cache-Control: public, max-age=300 -- semi-dynamic content
Cache-Control: private, no-store -- per-user data
ETag: "5d0a92f1-2ab3" -- conditional revalidationBest-practice patterns:
- For user-uploaded media: include the asset's content hash or version in the URL (
avatar.v17.webp) and set a longmax-age. New versions get new URLs - no cache invalidation needed. - For HTML/API responses: short TTLs (60s-5min) so changes propagate quickly.
- For per-user pages:
private, no-storeto keep the CDN out of it entirely.
Cache Invalidation
When you must invalidate (a leaked image, a corrected video):
- Versioned URL (preferred): change the URL, the old one is just orphaned in the cache.
- Purge by URL: instruct the CDN to evict a specific object. Slow (seconds to minutes globally), expensive, often rate-limited.
- Purge by tag/surrogate key: tag related objects when storing them, then purge by tag (Fastly, Cloudflare). Powerful and often free.
The order is important: prefer versioned URLs. Active purges are an emergency tool, not a default workflow.
End-to-End Media Pipeline
A typical flow for a photo-sharing app:
---------- Upload + delivery ----------
1. Client requests presigned upload URL from API
2. Client PUTs original to s3://uploads/raw/<id>.jpg
3. Upload triggers a Lambda / queue job:
- generate thumbnails (multiple sizes)
- convert to WebP / AVIF
- extract metadata (EXIF, dominant color)
- write derived assets to s3://uploads/derived/<id>/<size>.webp
4. Database row stores key paths and metadata
5. CDN (CloudFront / Cloudflare) sits in front of s3://uploads/derived/
6. Front-end uses URL like https://cdn.example.com/u/<id>/sm.webp
7. Browser hits nearest edge, edge serves cached objectFor video, the same idea but with HLS/DASH segments: the encoder produces 5-10 second .ts chunks, each becomes its own object, the player requests them in sequence, and the CDN happily caches every single segment.
Signed URLs for Private Content
Not all media is public. For paid or private content, the CDN can validate signed cookies or signed URLs at the edge:
- The application server signs a URL with an expiry and an optional IP/policy.
- The CDN verifies the signature before serving.
- Without a valid signature, the request is rejected at the edge - no origin call.
Netflix uses this pattern for video streams; Stripe uses it for invoice PDFs. The edge becomes the authorization boundary, not your origin.
Real-World Examples
How real systems implement this in production
Netflix runs its own CDN and embeds caching appliances directly inside ISP networks. Popular movies are pre-positioned based on regional forecasts, so the bytes never traverse the public internet during peak viewing.
Trade-off: Building a custom CDN is enormously expensive in capex and engineering, justified only by Netflix's traffic share. Smaller companies use public CDNs (CloudFront, Cloudflare, Fastly) and accept slightly higher per-GB costs.
Instagram stores raw uploads in object storage, fans out async pipelines that produce thumbnails, transcodes, and ML features, and serves through a CDN tightly integrated with feed ranking.
Trade-off: Pre-generating many derivatives multiplies storage cost but is essential for fast feed rendering. The bet is that storage is cheap and CPU-on-demand at view time would not meet latency targets.
Cloudflare R2 is S3-compatible object storage with no egress fees when delivered through Cloudflare's CDN. Many media-heavy SaaS products migrate user-content buckets to R2 to escape AWS egress charges.
Trade-off: R2 lacks some advanced S3 features (full IAM integration, certain analytics tools) and locks you into Cloudflare's ecosystem. Best as a content delivery store paired with S3 for operational data.
Quick Interview Phrases
Key terms to use in your answer
Common Interview Questions
Questions you might be asked about this topic
Browser asks the API for a multi-part upload session. API initiates the upload with the object store and returns presigned PUT URLs for each part. Browser uploads parts in parallel directly to S3, retrying any that fail. On completion, browser calls API again to finalize. The API then enqueues a transcoder job that produces HLS chunks at multiple bitrates, writes them back to S3, and updates the database with the playback manifest URL. CDN serves the HLS chunks with long TTLs.
Use signed URLs at the CDN edge. Application server signs a short-lived URL (5-15 min) with the user's permission embedded. CDN validates the signature at the edge and serves from cache without contacting origin if cached. The cache key includes the path but not the signature. This gives you authorization plus edge performance, with no origin scalability concern.
DNS resolves the CDN hostname to the closest edge in Tokyo. Edge checks its local cache - miss. Edge requests from origin shield (a single regional cache that consolidates upstream requests). Origin shield checks - miss. Origin shield fetches from S3 in us-east-1 (one trans-Pacific round trip). Object travels back through shield to Tokyo edge to user. Subsequent users in Asia hit Tokyo edge (cache hit) and complete in 20 ms.
Put a CDN in front of S3 - the CDN's origin pull is one fetch per cache miss, not per user. Use origin shield to further reduce origin requests. Set long Cache-Control max-age on versioned assets. For very high-traffic media, evaluate egress-free providers (Cloudflare R2, Bunny Storage) for user-content buckets. Lifecycle cold objects to lower-cost classes. Audit who is downloading large objects - sometimes it's an accidental script or an analytics job that should be running inside the same region.
For per-user dynamic content with no shared cache benefit (your account dashboard, real-time feeds), CDN caching adds latency without hit benefit. Mark those endpoints Cache-Control: private, no-store. Internal traffic between services in the same VPC also bypasses the CDN. And for tiny low-traffic sites, the CDN setup overhead might exceed the savings - though most modern CDNs (Cloudflare free tier) are essentially free, so the bar is low.
Interview Tips
How to discuss this topic effectively
Default to 'object store + CDN' for any blob: photos, videos, PDFs, exports. State it in one sentence and move on. Trying to store these in a relational database is a junior-level mistake.
Always mention presigned URLs for uploads. 'My server never proxies the bytes' shows you understand bandwidth economics.
Talk about cache TTL strategy explicitly: long TTL on versioned URLs, short TTL on dynamic content, no-store on private. Interviewers love precise cache headers.
Bring up storage classes when cost is mentioned. 'I'd lifecycle to Standard-IA after 30 days' demonstrates production awareness.
For private content, mention signed URLs at the edge. Treating the CDN as the authorization boundary - not just the origin - is a senior pattern.
Common Mistakes
Pitfalls to avoid in interviews
Storing user-uploaded files in a database BLOB/binary column
Databases are not built for multi-MB blobs - they bloat backups, kill replication speed, and push you toward expensive disk tiers. Store the bytes in object storage and put only the key and metadata in the database.
Proxying uploads through your application server
Your server's network bandwidth becomes the bottleneck. Use presigned URLs so the client uploads directly to the object store. Your server only orchestrates.
Relying on cache purge as the default invalidation strategy
Purges are slow, rate-limited, and asymmetric across edges. Use versioned URLs (asset.v17.webp) so updates produce new URLs and you never have to purge except for emergencies.
Forgetting that S3 buckets are NOT directories
S3 keys are flat strings; slashes are convention, not structure. Listing 'objects in a folder' is just a prefix scan. Designing keys with predictable prefixes (uploads/users/<id>/) makes lifecycle rules and access policies much cleaner.
Leaving everything in S3 Standard forever
Most uploaded content is read heavily for the first week, then almost never. Lifecycle to Standard-IA at 30 days and Glacier at 180 days. This is one of the highest-ROI cost optimizations in any media-heavy product.
