🎯 Requirements
Functional
- Upload single / bulk, resumable
- Variants: original, 1080p, 480p, thumb WebP/AVIF
- Metadata: EXIF, location, tags
- List, view, delete
Non-Functional
10M req/day99.99% Availability<100ms readS3 Durable⚡ Core Principle
Never process synchronously on upload path.
Client → API GW → Auth → Presigned S3 URL → Direct Upload → Kafka → Workers → S3 Processed → CDN
Presigned URL saves 80% bandwidth - app servers handle 0 bytes of image data. Biggest efficiency win.
🏗️ Components
| Component | Tech | Responsibility |
|---|---|---|
| API Gateway | ALB, NGINX | TLS, rate limiting, routing |
| Auth | JWT, OAuth2 | Short-lived upload tokens |
| Upload Service | Go stateless | UUIDv7 photo_id, presigned URL, PENDING row |
| S3 | S3 | bucket/user_id/yyyy/photo_id_original.jpg |
| Queue | Kafka / SQS | Decouple upload/processing |
| Workers | K8s, libvips | NSFW, resize, compress |
| Metadata DB | Postgres + Cassandra | Hash dedup, EXIF JSONB |
| Cache+CDN | Redis, CloudFront | Edge caching immutable |
Schema
CREATE TABLE photos ( photo_id UUID PRIMARY KEY, user_id UUID, hash VARCHAR(64) UNIQUE, s3_keys JSONB, status TEXT, exif JSONB, created_at TIMESTAMPTZ );
🔌 API Design
POST /v1/photos/init
-> { photo_id, uploadUrl, expiresAt }
PUT S3 uploadUrl (client direct)
GET /v1/photos/{id}
-> { urls: {thumb, medium, large}, status }
GET /v1/users/{id}/photos?cursor=&limit=50
DELETE /v1/photos/{id}
Headers: Idempotency-Key, X-Content-Hash📈 Scalability
- Resumable: Tus + Redis chunks
- Dedup: SHA256 instant return
- AVIF/WebP: 50% smaller
- Autoscale: KEDA on Kafka lag
- Read: CDN → Redis → DB 95% hit
- S3 Events: Push-based
1M *5MB=5TB raw +15TB variants=20TB/day. Lifecycle to S3-IA after 90d.
🛡️ Failure & Security
Security: Presigned 5min expiry, MIME sniff, virus scan, Rekognition NSFW, signed CDN URLs private.
Reliability: DLQ, backoff, idempotent workers, S3 versioning, replicas.