Frontend
│
│ POST /assets
│ Idempotency-Key: req-123
▼
Go Asset API
│
├─ authenticate / authorize
│
├─ generate candidate
│ asset_id = 123
│ object_key = assets/123/original.jpg
│
├─ atomic upsert ───────────────→ MongoDB
│ │
│ │ assets
│ │ {
│ │ asset_id: "123",
│ │ owner_id: "user-7",
│ │ idempotency_key: "req-123",
│ │ object_key:
│ │ "assets/123/original.jpg",
│ │ status: "UPLOADING"
│ │ }
│ │
│ │ UNIQUE
│ │ (owner_id, idempotency_key)
│ │
│ ├─ new request
│ │ → insert candidate
│ │
│ └─ retry
│ → return existing asset
│
├─ status = UPLOADING
│ → generate fresh pre-signed PUT URL
│ for asset.object_key
│
├─ status = PROCESSING / READY
│ → no upload URL
│
└─ return
{
asset_id,
status,
upload_url?
}
Frontend
│
│ PUT image bytes using pre-signed URL
▼
S3
│
├─ verify signature + expiration
├─ store: assets/123/original.jpg
│
└─ emit ObjectCreated
│
▼
SQS
│
│ stores S3 event message
│ {
│ bucket: ...,
│ object_key: assets/123/original.jpg,
│ sequencer: ...
│ }
│
▼
Lambda bridge
│
├─ poll SQS: message becomes temporarily invisible (SQS visibility timeout)
│
├─ parse S3 event
│
├─ derive asset_id
│ assets/123/original.jpg
│ ↓
│ asset_id = 123
│
├─ derive stable event_id
│ from S3 event identity
│
├─ build AssetUploaded event
│ {
│ event_id: evt-A,
│ asset_id: 123,
│ object_key: assets/123/original.jpg
│ }
│
└─ publish to Kafka
│
├─ publish succeeds
│ │
│ ▼
│ Lambda returns SUCCESS (Delete SQS message automatically)
│
└─ publish fails/timeout
│
▼
Lambda returns ERROR (SQS message becomes visible again)
│
└─ after too many failures → SQS DLQ
Kafka
│
│ key = asset_id = 123
│
│ AssetUploaded
│ partition = 7
│ offset = 314
│ {
│ event_id: evt-A,
│ asset_id: 123,
│ object_key: assets/123/original.jpg
│ }
▼
ECS Asset processor
│
├─ atomically claim event ─────────────→ MongoDB
│ │
│ │ consumer_inbox
│ │ {
│ │ consumer: "asset-processor",
│ │ event_id: "evt-A",
│ │ state: "PROCESSING",
│ │ lease_owner: "worker-7",
│ │ lease_until: ...
│ │ }
│ │
│ │ unique:
│ │ (consumer, event_id)
│ │
│ ├─ COMPLETED
│ │ → duplicate already handled
│ │ → commit Kafka offset
│ │
│ ├─ PROCESSING + valid lease
│ │ → another worker owns it
│ │ → DO NOT process
│ │ → DO NOT commit offset
│ │
│ └─ missing / expired lease
│ → acquire lease
│ → continue
│
├─ update asset ───────────────────────→ MongoDB
│ │
│ └─ asset.status = PROCESSING
│
├─ GET original asset ────────────────→ S3
│
├─ validate / resize / classify / transform
│
├─ PUT derived assets ────────────────→ S3
│ │
│ └─ deterministic object keys
│ assets/123/thumb.jpg
│
├─ retryable failure
│ │
│ ├─ release lease / let lease expire
│ ├─ keep inbox state non-COMPLETED
│ └─ DO NOT commit Kafka offset
│ │
│ ▼
│ retry
│
├─ permanent failure
│ │
│ └─ MongoDB transaction
│ │
│ ├─ asset.status = FAILED
│ │
│ └─ consumer_inbox
│ {
│ event_id: "evt-A",
│ state: "COMPLETED"
│ }
│
│ transaction commits
│ │
│ └─ commit Kafka offset
│
└─ success
│
├─ generate AssetReady event
│ event_id = evt-B
│ asset_id = 123
│ causation_id = evt-A
│
└─ MongoDB transaction
│
├─ asset.status = READY
│
├─ consumer_inbox
│ {
│ event_id: "evt-A",
│ state: "COMPLETED"
│ }
│
└─ insert outbox_events
{
event_id: "evt-B",
type: "AssetReady",
asset_id: "123",
causation_id: "evt-A",
published: false
}
transaction commits
With MongoDB, horizontal scaling usually means sharding.
MongoDB cluster
├─ Shard 1
├─ Shard 2
└─ Shard 3
Shard 1 replica set
├─ Tokyo
├─ Tokyo
└─ Singapore
Shard 2 replica set
├─ Tokyo
├─ Singapore
└─ Sydney
You decide or configure how data is distributed
AWS handles the underlying horizontal scaling. Normally, no cross-region replication. (CDN needs to come in)