Event-Carried State Transfer
establishedSummary
Embed the current state of the changed entity directly in each event payload ("fat event"), so consuming services can update their local read models without making synchronous API calls back to the source service: eliminating downstream query fan-out and reducing coupling between services.
Problem
Thin events (ID-only) require consuming services to call back to the source service for entity data, creating synchronous coupling, a query fan-out under high event rates, and cascading failures if the source service is unavailable. ECST eliminates this by carrying the data in the event itself.
Description
In event-driven systems, services emit events when their state changes. A "thin event" carries only a reference: {"type": "order.updated", "order_id": "abc123"}. Consuming services that need order details must call the Order service synchronously to fetch them. Under high event rates, this produces a downstream query storm: every event triggers one or more round-trip API calls back to the source.
Event-Carried State Transfer (ECST) embeds the current entity state in the event: {"type": "order.updated", "order_id": "abc123", "status": "shipped", "total": 99.99, ...}. Consuming services update their local projections directly from the event payload with no callback needed. The Order service emits once; all consumers derive state locally.
ECST is particularly valuable for read model projections in CQRS architectures: the query side maintains a denormalized read model rebuilt from event streams. When an order is updated, the search service, analytics service, and notification service all update their own representations using the event payload: no synchronous call back to the Order service.
Trade-off: events become larger. For entities with large state (documents, configurations), the event payload can grow to hundreds of kilobytes. Kafka's default message size limit (1 MB) must be respected. For very large entities, a reference to a content-addressed snapshot store (S3 object key) may replace the inline payload.
Distinction from event sourcing: ECST events carry current state, not deltas. Event sourcing events carry the change (what happened). ECST consumers get the full current state and overwrite their projection; event sourcing consumers apply deltas incrementally.
An Avro or Protobuf schema for a fat order event typically looks like:
message OrderUpdatedEvent {
string event_id = 1;
string order_id = 2;
google.protobuf.Timestamp occurred_at = 3;
OrderStatus status = 4;
repeated OrderItem items = 5;
int64 total_cents = 6;
string customer_id = 7;
}
A schema registry (Confluent or Apicurio) registers the event schema to enforce backward compatibility, so consumers can read events produced by both older and newer schema versions. For large entities, store the full payload in S3 or GCS with a content-addressed key and embed only the key and metadata in the event: {"type": "document.updated", "doc_id": "...", "version": 42, "payload_key": "s3://bucket/docs/abc123/v42.json"}.
Tradeoffs
Eliminates the synchronous callback from consumer to source; fully decoupled asynchronous processing
Source service unavailability does not block consumers from processing events already in the log
Event replay from the log is sufficient to rebuild any consumer's projection
Reduces latency for downstream projections since there is no network round trip for a data fetch
Larger event payloads increase message broker storage and bandwidth costs
Events may carry sensitive fields that not all consumers should see, requiring event filtering or schema evolution
If the entity updates multiple times in rapid succession, in-flight events carry stale state for intervening versions
Schema evolution for embedded state is harder than for thin events, since all consumers must handle old and new event schemas
When to use
Multiple downstream services need to maintain read projections of the same entity
Each service can update its projection directly from the event without a callback
Source service availability should not block downstream processing
If the source is unavailable, thin events result in blocked consumers; ECST events are self-contained
Event rate is high and synchronous callbacks from consuming services would overload the source
1000 events/sec × 5 consumers = 5000 synchronous API calls back to the source
Entity state is small enough to fit in the event broker's message size limit
Entities larger than ~100KB may require a hybrid approach (inline metadata + S3 reference for large fields)
When not to use
Entity state is large (megabytes), making fat events expensive to produce and replicate
Embed a reference to a snapshot store and fetch only when needed
Consumers only need the event notification, not the entity state
If consumers react to events but don't need the data (e.g., cache invalidation), thin events are simpler
The event represents an action or command semantics, not a state update
ECST is appropriate for state change events; command events have different semantics
Operational Requirements
Keep event schema evolution backward compatible
Old consumers must read new events, and new consumers must handle replayed old events.
Monitor event payload sizes
A sudden increase in entity size can cause broker rejection at the message size limit.
Apply field-level security for sensitive event data
Required if events carry PII or other sensitive data that not all consumers should access.
Characteristics
Relationships
Complements
Basis
Event-Carried State Transfer is documented in Enterprise Integration Patterns and Martin Fowler's bliki; widely discussed in microservices and event-driven architecture literature
Related Architecture Knowledge
Outbound: this entity affects
Fat events produced via the outbox pattern carry entity state that consumers can use to update projections without calling back to the source service.
Full relationship →Inbound: affects this entity
Kafka topics serve as the delivery mechanism for fat event payloads; Kafka's compacted topics can retain the latest state per key, enabling exactly the event-carried state transfer pattern.
Full relationship →