Hello. In the previous lesson, you defined an event contract: a producer-owned business fact wrapped in metadata for identity, source, time, correlation, and tracing. We ended with an important design question: how much of the business state should that event include?
This lesson distinguishes two closely related patterns:
- event notification: “Something relevant changed.”
- event-carried state transfer (ECST): “Something changed, and here is the state you need to act without querying me.”
Both can use Kafka topics, RabbitMQ exchanges, or the same CloudEvents-style envelope. The difference is not the broker or the event name. It is whether the consumer can do its intended work using data in the event and its own local state, without a synchronous callback to the producer.
By the end, you should be able to classify a workflow correctly and defend the choice in a Technical Lead interview.
The decisive question: must the consumer call the producer?
Consider a Customer service that owns customer addresses and a Billing service that needs the current billing address when preparing an invoice.
With event notification, Customer service emits a compact fact:
{
"type": "com.acme.customer.address-changed.v1",
"id": "E-1842",
"source": "urn:acme:customers",
"subject": "customers/C-881",
"time": "2025-03-08T10:14:26Z",
"data": {
"customerId": "C-881"
}
}
Billing receives the event, recognizes that C-881 changed, then calls:
GET /customers/C-881/billing-address
The event says, “pay attention”; the API call obtains the state. The customer system remains authoritative, but Billing’s reaction still has a runtime dependency on Customer service.
With event-carried state transfer, the event carries the relevant state:
{
"type": "com.acme.customer.billing-address-changed.v1",
"id": "E-1843",
"source": "urn:acme:customers",
"subject": "customers/C-881",
"time": "2025-03-08T10:14:26Z",
"data": {
"customerId": "C-881",
"version": 42,
"billingAddress": {
"line1": "18 Market Street",
"city": "Leeds",
"postalCode": "LS1 4AB",
"country": "GB"
}
}
}
Billing can persist that address in a local customer_billing_profile table and prepare future invoices from its own database. It does not own the customer’s address; it owns only its local, purpose-specific replica.
The classification rule is therefore:
| Question | Notification | Event-carried state transfer |
|---|---|---|
| What does the event principally say? | A relevant fact occurred. | A fact occurred and provides usable state. |
| Can the consumer complete its intended reaction without querying the producer? | Usually no. | Yes, for the defined use case. |
| Does the consumer keep a local replica? | Perhaps after fetching. | Yes, normally. |
| Runtime dependency after consumption | Callback remains. | Callback is removed for this data need. |
| Primary cost | Callback traffic and producer availability dependency. | Data replication, schema responsibility, and eventual consistency. |
The payload size alone does not settle the question. An event containing a customer name and ID is still a notification if the consumer must call Customer service for the address it needs. Conversely, a compact event can be ECST if those few fields are genuinely sufficient for the consumer’s task.
{
"type": "exercise",
"id": "908966f5-639e-4142-820b-0496ddc7bac5"
}
Watch the contrast in one concrete example
The following segment visualizes the exact distinction: notification followed by a fetch, then an event containing the state necessary to update a local copy.
{"type":"video","title":"Event Carried State Transfer: Keep a local cache!","learning_duration":228,"video_id":"IzBEbfSg0uY","par_intro":"Watch “Event Carried State Transfer: Keep a local cache!” by CodeOpinion. It illustrates the operational reason for this choice: eliminating a callback removes temporal coupling, not merely an API call.","par_directions":"From <span data-type=\"resource_video_timerange\" data-resource-subitem-id=\"95f8aec2\" data-range-start=\"36\" data-range-end=\"264\">notification versus transfer</span>, follow the example in which one service receives a change notification, calls back to fetch details, and maintains a local cache. Focus on the two failure modes of the callback approach: request volume against the producer and a stale cache when the callback cannot succeed.","video_duration":709,"isV2":true,"blockId":"51dcc8ee-a8e6-4c9e-b99d-f27a27b44d2c","lessonId":"a35d3c61-235e-4307-b486-83a10a48aee5"}
The term temporal coupling matters in interviews. Billing may be logically dependent on customer address data, but ECST means it is not dependent on Customer service being available at the exact time Billing processes an invoice request. Its local replica may be slightly behind, but it remains available.
This is a trade, not a free improvement:
- Notification centralizes reads at the owner and can provide fresher data after a successful callback.
- ECST decentralizes read availability by accepting replicated, potentially stale data.
{"type":"image","url":"https://eda-visuals.boyney.io/assets/visuals/eda/event-types.png","caption":"The visual contrasts a small notification payload with event-carried state transfer, delta events, and domain events. Notification and state transfer answer how much consumer-usable data crosses a service boundary; delta events describe one possible way to scope that state.","isV2":true,"blockId":"bcb4d604-b642-4122-b239-37694586401b","lessonId":"a35d3c61-235e-4307-b486-83a10a48aee5"}
A workflow classification method
When an interviewer gives you a vague prompt such as, “Customer updates should be available to several services,” do not answer immediately with “publish an event.” First work through five questions.
1. What business decision will the consumer make?
Start with the consumer’s actual task, not the producer’s entity model.
For example:
- Fraud service needs customer country and account-risk tier to evaluate a transaction.
- Shipping service needs the selected shipping address for a particular order.
- Analytics needs an immutable record of a completed order.
- Notification service only needs to know that a password-reset request was created, then sends a message through its own preconfigured channel.
A consumer’s decision determines the minimum useful state. “Publish the entire Customer Java entity” is not an answer; it is usually accidental coupling and sometimes a security incident.
2. Can the consumer tolerate a synchronous lookup?
A callback may be reasonable when all of the following are true:
- the reaction is low volume;
- it is not latency-sensitive;
- the consumer already needs a strongly current answer;
- the producer can safely serve the query;
- failure can be retried or deferred without harming the workflow.
For instance, an internal audit service receiving AccountClosed(accountId) may fetch a complete archival record once, at low volume. A notification can be enough.
A callback is a poor fit when:
- many consumers react to frequent updates;
- the producer is a critical bottleneck;
- the consumer must continue operating during producer outages;
- user-facing latency would include a remote dependency;
- the consumer needs the same reference data repeatedly.
This is where ECST earns its extra design and operational cost.
3. Which state is legitimate to replicate?
The producer remains the source of truth. A consumer should receive only the state it needs to perform its own business function.
For Billing, that may be:
{
"customerId": "C-881",
"billingAddress": { "...": "..." },
"taxRegion": "GB-ENG",
"version": 42
}
It should not automatically receive:
- authentication credentials or password-reset tokens;
- payment-card data;
- internal moderation flags;
- full marketing preferences if Billing has no legitimate need;
- database implementation fields.
The event contract is a cross-team public interface. Every field becomes a dependency, a privacy obligation, and a schema-evolution responsibility.
4. Is the consumer acting on a current fact or coordinating a workflow step?
This question prevents a common misuse: copying large transactional objects into events merely because “events should be self-contained.”
Suppose a checkout flow works as follows:
- The customer creates a draft order.
- Payment service receives and stores payment details directly through its own API.
- The customer confirms the checkout.
- Order service emits
OrderPlaced. - Payment service authorizes the already-stored payment method for that order.
OrderPlaced can be a notification:
{
"type": "com.acme.order.placed.v1",
"data": {
"orderId": "O-4821"
}
}
Payment service already owns the payment-method data it requires. It does not need Order service to broadcast sensitive card-related information. The event triggers a workflow step; it is not a distribution channel for all checkout data.
If Order service sends a giant payload containing payment details “so Payment can process the order,” that is a warning sign. The data likely belongs in Payment’s boundary and should have entered that boundary through an explicit, secure interaction earlier in the workflow.
5. What is the consequence of stale replicated state?
ECST creates an eventually consistent replica. At a given instant, Customer service may be at address version 43 while Billing has processed only version 42.
For some decisions, that is acceptable:
- showing a default billing address that a user can confirm;
- calculating an estimate;
- enriching a notification;
- serving a search or reporting view.
For others, it may not be:
- regulatory checks requiring the currently authoritative identity record;
- a financial action where outdated risk status is unacceptable;
- a workflow that must reject an action based on the exact latest state.
In those cases, a direct query to the owner at the decision boundary—or a different workflow design—may be necessary. ECST does not manufacture strong consistency.
{
"type": "exercise",
"id": "6e031003-4408-4cf9-8826-4dac83b83721"
}
Read the underlying trade-off
Martin Fowler’s explanation is concise and useful for articulating the decision in architecture discussions.
{"type":"reading","par_intro":"Read Martin Fowler’s discussion of event notification and event-carried state transfer. It provides a precise vocabulary for explaining why local replicas improve resilience and latency while introducing replica-maintenance responsibility.","par_directions":"In the “Event Notification” section, read <span data-type=\"resource_reading_textrange\" data-resource-subitem-id=\"0ece0058\" data-range-start=\"This happens when a system sends event messages\" data-range-end=\"to decide what to do next\">the notification pattern</span>, paying attention to the absence of an expected response and the callback to obtain detail. Then read the full “Event-Carried State Transfer” section, beginning <span data-type=\"resource_reading_textrange\" data-resource-subitem-id=\"c439c055\" data-range-start=\"This pattern shows up when you want to update clients\" data-range-end=\"just to call the sender for more information when needed\">the state-transfer trade-off</span>. Focus on the line between the two patterns: whether the recipient needs to contact the source to do future work.","learning_duration":"10 minutes","url":"https://martinfowler.com/articles/201701-event-driven.html","title":"What do you mean by “Event-Driven”? - Martin Fowler","isV2":true,"blockId":"8559d5db-18fa-48bc-b255-667fb9eab76e","lessonId":"a35d3c61-235e-4307-b486-83a10a48aee5"}
Two consequences deserve emphasis.
First, ECST improves a consumer’s operational autonomy, not its ownership. Billing can use a local address replica while Customer service remains the authority that determines whether the address is valid and what its current canonical form is.
Second, event notification is not inherently inferior. It has smaller, more stable payloads and reduces replication work. Use it when the consumer only needs awareness, already owns the needed data, or deliberately needs to consult the authority.
State transfer comes in two useful shapes
“Carries state” does not necessarily mean “contains the entire entity.” The state can be expressed as a snapshot or a delta.
Snapshot: set the consumer’s relevant state
A snapshot gives the relevant current representation after the change.
{
"type": "com.acme.customer.shipping-profile-updated.v1",
"data": {
"customerId": "C-881",
"version": 42,
"shippingProfile": {
"recipientName": "Asha Patel",
"address": {
"line1": "18 Market Street",
"city": "Leeds",
"postalCode": "LS1 4AB",
"country": "GB"
}
}
}
}
A consumer can apply the result as “customer C-881’s shipping profile is now this state.” Reapplying the same snapshot is naturally safer than repeating an instruction such as “increment quantity by one.”
Delta: communicate the changed fields
A delta contains only the difference:
{
"type": "com.acme.customer.shipping-address-changed.v1",
"data": {
"customerId": "C-881",
"version": 42,
"newAddress": {
"line1": "18 Market Street",
"city": "Leeds",
"postalCode": "LS1 4AB",
"country": "GB"
}
}
}
A delta can be smaller and highly expressive: it states exactly what changed. But the consumer may need a pre-existing local record and must correctly merge the change.
The contrast is practical:
| Design | Strength | Main risk |
|---|---|---|
| Notification | Small contract; consumer fetches current authority state when needed. | Callback load and runtime availability dependency. |
| Delta transfer | Focused payload; less redundant data. | Consumer must maintain prior state and merge updates correctly. |
| Snapshot transfer | Straightforward local replacement; easier recovery and idempotent application. | More data per event; payload can become bloated. |
| “Fat” event containing unrelated convenience data | Can simplify one consumer temporarily. | Producer becomes coupled to accumulating consumer-specific needs. |
A snapshot is not automatically a “fat event.” It becomes problematic when it contains broad, unrelated data added ad hoc for particular consumers rather than a coherent representation owned by the producer and justified by a stable integration need.
The Deloitte Engineering article calls out this tension: delta events can preserve focused intent, whereas unnecessary state added for individual consumers tends to bloat the contract.
{"type":"reading","par_intro":"Read Deloitte Engineering’s treatment of event-carried state transfer for its practical distinction between notification, local replicas, delta events, and overly broad “fat” events.","par_directions":"Start in “The characteristics of the Event-Carried State Transfer pattern.” Read <span data-type=\"resource_reading_textrange\" data-resource-subitem-id=\"6cf7124e\" data-range-start=\"As the name suggests, the main characteristic\" data-range-end=\"high throughput, robustness, and availability requirements\">the pattern characteristics</span>, especially the reason a consumer keeps a private replica. Then, in “Event design,” read from <span data-type=\"resource_reading_textrange\" data-resource-subitem-id=\"f86cd1c5\" data-range-start=\"When using the event-notification pattern, events are quite small\" data-range-end=\"higher awareness of the producers business processes\">the delta and fat event comparison</span>. Finally, read <span data-type=\"resource_reading_textrange\" data-resource-subitem-id=\"937404f5\" data-range-start=\"Many event brokers offer at-least-once processing by default\" data-range-end=\"take advantage of it when possible\">the duplicate-processing discussion</span> to see why a state-transfer consumer should apply repeated updates safely.","learning_duration":"12 minutes","url":"https://deloitte-engineering.github.io/2021/the-event-carried-state-transfer-pattern","title":"The Event-Carried State Transfer pattern","isV2":true,"blockId":"ecebe6a6-d4df-466d-b659-80fa0bfe83e8","lessonId":"a35d3c61-235e-4307-b486-83a10a48aee5"}
The hidden engineering obligations of ECST
A local replica makes one dependency disappear at runtime, but it creates responsibilities that must be designed explicitly.
The replica is eventually consistent
Your consumer must behave correctly while its replica is behind. Document the acceptable staleness in business terms:
- “The shipping quote may use an address replica up to five minutes old; the customer confirms it before purchase.”
- “The fraud decision must use the authoritative latest risk status; do not rely solely on the replica.”
Do not claim ECST gives both “no dependency” and “always current data.” Those promises conflict in a distributed system.
Updates require a version or ordering rule
Imagine CustomerUpdated version 41 and version 42. If concurrent processing lets version 41 finish last, a naïve consumer could overwrite newer state with older state.
Include a producer-defined version, sequence, or other reliable ordering marker in state-transfer events. Then apply an update only if it is newer than the stored version.
Conceptually, a consumer’s local projection behaves like this:
If incoming version is greater than stored version:
replace or merge the local representation
Otherwise:
ignore it as stale or already applied
The exact implementation will depend on the database and broker. Later lessons cover ordering scope, duplicate delivery, and idempotent consumers in detail. For now, recognize that a state replica needs more than a consumer method that blindly calls repository.save().
Duplicate delivery must not change the result
Brokers commonly provide at-least-once delivery. A consumer may persist the update successfully but fail before acknowledging it, then receive the same event again.
For a snapshot event, “set address to this value at version 42” is generally naturally idempotent. For a delta event, ensure that applying it twice has the same result, or retain the producer event ID and reject duplicates.
New consumers need a bootstrap plan
A new Billing service cannot build a complete customer-address replica from events emitted only after its deployment. It needs one of these strategies:
- replay a retained stream from a known point;
- consume a compacted/latest-state topic where appropriate;
- obtain an initial snapshot, then transition to live events safely;
- temporarily query the producer while its replica catches up.
This is especially relevant to Kafka, where retention and replay support state reconstruction. RabbitMQ is often used for transient work delivery rather than long historical replay, so the surrounding design must provide any required bootstrap data elsewhere.
{
"type": "exercise",
"id": "aba8f9ab-5f6a-4153-8798-ae0ff79794b5"
}
Worked classifications
The following examples resemble the reasoning expected in an interview. Notice that the answer starts with the consumer’s decision and the consequences of stale data—not with a broker feature.
Scenario A: Address change updates insurance pricing
Workflow: Customer service owns addresses. Pricing service recalculates insurance quotes after an address change. It needs the new address and regional classification to calculate locally. Pricing must remain available if Customer service is temporarily unavailable.
Choice: Event-carried state transfer.
{
"type": "com.acme.customer.address-changed.v1",
"data": {
"customerId": "C-881",
"version": 42,
"newAddress": { "...": "..." },
"riskRegion": "R3"
}
}
Reasoning: Pricing can maintain the subset of customer state required for its calculation. This removes callback latency, avoids a thundering herd of fetches after bulk address imports, and isolates pricing availability from Customer service. The team must accept that the local replica can lag and must apply versions safely.
Scenario B: A completed order should trigger payment authorization
Workflow: Payment service already stores a tokenized payment method for O-4821. Order service publishes OrderPlaced only after checkout confirmation. Payment needs to know that it may now authorize the payment.
Choice: Event notification.
{
"type": "com.acme.order.placed.v1",
"data": {
"orderId": "O-4821"
}
}
Reasoning: The event is a completed fact that advances a workflow. Payment already owns the payment data; it should not receive it through Order service. If Payment requires some order facts for validation, add only the facts that Order service legitimately owns and Payment needs—not a full order and payment dump.
A further design review may decide this interaction is better expressed as an explicit AuthorizePayment command, because the workflow requires a specific service to act. That command-versus-event distinction was covered earlier; it is separate from notification versus ECST.
Scenario C: Search service maintains product catalog results
Workflow: Catalog service owns products. Search service must return product names, prices, availability, and category filters at high query volume. Querying Catalog for each search result would add latency and overload it.
Choice: Event-carried state transfer, typically snapshot-style.
{
"type": "com.acme.catalog.product-published.v1",
"data": {
"productId": "P-104",
"version": 17,
"name": "Wireless Keyboard",
"price": { "amount": 49.99, "currency": "USD" },
"available": true,
"categories": ["accessories", "keyboards"]
}
}
Reasoning: Search owns a projection optimized for searching, while Catalog retains authority over products. Slightly stale search results are generally acceptable. A product’s availability at final checkout, however, may require a fresh reservation or validation in the order workflow; the search replica should not be treated as an inventory authority.
Scenario D: Security audit after an account is closed
Workflow: Identity service closes an account. Audit service must record that closure and can retrieve a formal archival record later through a controlled internal process.
Choice: Usually event notification.
{
"type": "com.acme.identity.account-closed.v1",
"data": {
"accountId": "A-990",
"reasonCode": "CUSTOMER_REQUEST"
}
}
Reasoning: Audit’s immediate responsibility is to record the closure fact. It does not need Identity service’s complete account state embedded in a widely distributed event. The minimal event reduces exposure of sensitive identity data. If audit needs further information, its controlled archival retrieval path can be designed separately.
A concise interview decision statement
For a given workflow, use this structure:
“I would use event notification when the consumer only needs awareness of the fact, already owns the data needed to react, or intentionally needs a fresh authoritative lookup.
I would use event-carried state transfer when the consumer needs a stable subset of producer-owned data at high volume or high availability, and can accept an eventually consistent local replica. I would include only consumer-relevant state, a producer-defined version, a unique event ID, and a replay/bootstrap plan. The producer remains the source of truth.”
That response demonstrates that you understand the architectural decision, reliability implications, data ownership, and operational consequences—not merely the two payload shapes.
Key takeaways
Event notification and ECST both publish facts, but they support different consumer models:
- A notification tells consumers that something happened; they may fetch state or act using data they already own.
- Event-carried state transfer gives consumers enough producer-owned state to maintain a purpose-specific local replica and continue work without querying the producer.
- The important test is whether the consumer can perform its intended task without a callback, not whether the payload looks “small” or “large.”
- ECST improves latency, resilience, and producer load characteristics, but introduces replicated data, eventual consistency, versioning, duplicate-handling, and bootstrap responsibilities.
- Do not use ECST to broadcast data that belongs in another service’s domain, especially sensitive workflow data such as payment details.
Next, you will move from payload design to delivery semantics: choosing at-most-once, at-least-once, or effectively-once processing according to the business consequence of loss and duplication.
Can't find a good explanation? Sign up and we'll make it for you