REST Payloads vs Event Payloads
| Aspect | REST API Payload | Event Payload |
|---|---|---|
| Delivery | Synchronous request-response | Asynchronous, fire-and-forget |
| Audience | Single known consumer | Multiple unknown consumers |
| Coupling | Tight (caller knows endpoint) | Loose (publisher doesn't know consumers) |
| Retries | Client retries the HTTP call | Broker retries delivery or routes to DLQ |
| Versioning | URL path or Accept header | Event type name or schema version field |
| Size | Can be large (pagination, full resources) | Should be small (reference IDs, not full entities) |
Important
Event Envelope Design
1{2 "specversion": "1.0",3 "id": "evt_01HXY9Z2ABC3DEF4",4 "type": "order.created",5 "source": "order-service",6 "time": "2026-04-15T14:30:01.234Z",7 "datacontenttype": "application/json",8 "subject": "order-5678",9 "correlation_id": "req_01HXYZ9ABC",10 "data": {11 "order_id": "order-5678",12 "user_id": "user-1234",13 "status": "pending",14 "total": 149.99,15 "currency": "USD",16 "items": [17 { "product_id": "prod-001", "quantity": 2, "price": 49.99 },18 { "product_id": "prod-002", "quantity": 1, "price": 50.01 }19 ],20 "created_at": "2026-04-15T14:30:01.234Z"21 }22}CloudEvents Specification
CloudEvents is a CNCF standard that defines a common envelope for event messages. Using CloudEvents ensures your events are interoperable across brokers (Kafka, RabbitMQ, AWS EventBridge, Google Pub/Sub):
| Field | Required | Description | Example |
|---|---|---|---|
| specversion | Yes | CloudEvents spec version | 1.0 |
| id | Yes | Unique event identifier | evt_01HXY9Z2ABC |
| type | Yes | Event type (reverse DNS or dot notation) | com.example.order.created |
| source | Yes | Event producer URI | /order-service |
| time | No* | Timestamp (RFC 3339) | 2026-04-15T14:30:01Z |
| datacontenttype | No* | Content type of data | application/json |
| subject | No | Subject of the event (entity ID) | order-5678 |
| data | No | Event payload | { "order_id": "..." } |
Time and datacontenttype
time and datacontenttype are technically optional in the CloudEvents spec, always include them. Every event should have a timestamp, and declaring the content type prevents parsing ambiguity.Event Naming Conventions
1// Pattern: domain.entity.action (past tense)2"type": "order.created"3"type": "order.payment.succeeded"4"type": "order.payment.failed"5"type": "order.shipped"6"type": "order.delivered"7"type": "order.cancelled"8"type": "order.refund.initiated"910// Pattern: reverse DNS (formal / enterprise)11"type": "com.example.commerce.order.created"12"type": "com.example.inventory.stock.depleted"1314// Anti-patterns to avoid:15"type": "createOrder" // imperative (command, not event)16"type": "ORDER_CREATED" // inconsistent casing17"type": "order-was-created" // verbose, inconsistent separatorSchema Evolution
Backward-Compatible Changes
1// v1: Original event2{3 "type": "order.created",4 "data": {5 "order_id": "order-5678",6 "user_id": "user-1234",7 "total": 149.998 }9}1011// v2: Added optional "discount" field (backward compatible)12{13 "type": "order.created",14 "data": {15 "order_id": "order-5678",16 "user_id": "user-1234",17 "total": 149.99,18 "discount": 10.00,19 "coupon_code": "SAVE10"20 }21}2223// v1 consumers ignore the new fields — no breakage24// v2 consumers check for the new fields and handle their absenceHandling Breaking Changes
1// WRONG: Changing "total" from number to object in "order.created"2// This breaks all existing consumers34// RIGHT: Create a new event type5{6 "type": "order.created.v2",7 "data": {8 "order_id": "order-5678",9 "pricing": {10 "subtotal": 149.99,11 "discount": 10.00,12 "tax": 12.60,13 "total": 152.5914 }15 }16}1718// Publish both v1 and v2 during migration period19// Consumers migrate at their own pace20// Deprecate v1 after all consumers have migratedDead Letter Queues
Events end up in the DLQ for several reasons:
| Failure Type | Cause | Resolution |
|---|---|---|
| Parse failure | Invalid JSON (truncated, malformed) | Fix producer serialization, replay from DLQ |
| Schema violation | Missing required field, wrong type | Update consumer or fix producer schema |
| Business rejection | Invalid state transition, unknown entity | Investigate data inconsistency, manual fix |
| Infrastructure | Database down, timeout, network failure | Wait for recovery, automatic retry + replay |
Event Payload Anti-Patterns
1. Embedding Full Entities
1// ANTI-PATTERN: Embedding the full user object (bloated, stale data)2{3 "type": "order.created",4 "data": {5 "order_id": "order-5678",6 "user": {7 "id": "user-1234",8 "name": "Alice Smith",9 "email": "[email protected]",10 "address": { "street": "123 Main St", "city": "..." },11 "preferences": { ... },12 "payment_methods": [ ... ]13 }14 }15}1617// CORRECT: Reference by ID, let consumers fetch what they need18{19 "type": "order.created",20 "data": {21 "order_id": "order-5678",22 "user_id": "user-1234",23 "total": 149.9924 }25}2. Missing Correlation IDs
Without a correlation_id, tracing an event chain across services is impossible. When a user places an order, the chain might be: order.created → payment.charged → inventory.reserved → shipping.initiated. The correlation_id links all of them.
Real-World Example: E-Commerce Event Stream
1// 1. Order created2{3 "specversion": "1.0",4 "id": "evt_001",5 "type": "order.created",6 "source": "order-service",7 "time": "2026-04-15T14:30:00Z",8 "correlation_id": "corr_abc123",9 "subject": "order-5678",10 "data": {11 "order_id": "order-5678",12 "user_id": "user-1234",13 "items": [{ "product_id": "prod-001", "quantity": 2, "price": 49.99 }],14 "total": 99.98,15 "currency": "USD"16 }17}1819// 2. Payment processed20{21 "specversion": "1.0",22 "id": "evt_002",23 "type": "order.payment.succeeded",24 "source": "payment-service",25 "time": "2026-04-15T14:30:05Z",26 "correlation_id": "corr_abc123",27 "subject": "order-5678",28 "data": {29 "order_id": "order-5678",30 "payment_id": "pay_xyz789",31 "amount": 99.98,32 "method": "credit_card",33 "card_last_four": "1234"34 }35}3637// 3. Inventory reserved38{39 "specversion": "1.0",40 "id": "evt_003",41 "type": "inventory.reserved",42 "source": "inventory-service",43 "time": "2026-04-15T14:30:06Z",44 "correlation_id": "corr_abc123",45 "subject": "order-5678",46 "data": {47 "order_id": "order-5678",48 "reservations": [49 { "product_id": "prod-001", "warehouse": "us-east-1", "quantity": 2 }50 ]51 }52}Best Practices
- ✓Use the CloudEvents envelope standard for cross-service interoperability
- ✓Include
correlation_idin every event for tracing event chains - ✓Name events in past tense (order.created, not createOrder) — events describe facts
- ✓Make schema changes additive only: add optional fields, never remove or rename
- ✓Use a Schema Registry to validate events before they reach consumers
- ✓Configure dead letter queues for every consumer to prevent data loss
- ✗Don't embed full entities — use reference IDs and let consumers fetch what they need
- ✗Don't use event payloads as commands — events describe what happened, not what to do
- ✗Don't change existing field types — create a new event version instead