skip to content

Should a domain event's payload carry the full state of the aggregate at the time it was raised, or just the minimal data needed to describe what changed, like IDs and the specific delta? What are the trade-offs?

level: seniorimportance: should knowfreq 45%

answer

  1. thin = IDs + relevant delta; fat = broad snapshot
  2. fat events risk staleness if processed late
  3. thin events force fresh lookup, avoid stale overwrite
  4. fat events over-expose fields to every listener
  5. fat snapshot common at integration layer, not domain layer

basics

~20 s

A 'thin' event carries just the key IDs and what changed, like an order ID and new status. A 'fat' event carries the whole aggregate's data. Thin events are smaller and less likely to leak stale info; fat events save listeners an extra lookup but can go stale or expose more than needed.

solid answer

~50 s

Thin events - carrying identifiers plus just the fields relevant to the specific fact, e.g. OrderShipped{orderId, trackingNumber, shippedAt} - keep the event small, stable, and unlikely to leak unrelated data, but require listeners that need more context to look the aggregate back up, costing a round trip and risking a race if the aggregate has changed again by the time they read it. Fat events - embedding a larger snapshot - save that round trip and let listeners work entirely off the event, but grow the event's coupling surface to every field any consumer might need, risk exposing data a listener shouldn't see, and can silently go stale if the listener processes the event later than expected. The general default in DDD is thin, intent-revealing events; fat/snapshot events are a deliberate optimization, often at the integration-event layer rather than the in-model domain event.

go deeper

for a junior

Should understand that an event doesn't have to carry everything about the aggregate, just what's relevant.

for a middle

Should be able to describe the extra-lookup cost of thin events versus the coupling cost of fat events.

for a senior

Should choose payload shape deliberately per event, and recognize the stale-snapshot failure mode in fat events.

for a principal

Should set the boundary convention, thin in-model domain events and fatter versioned integration events where warranted, and recognize when a team is accidentally growing domain events into an informal second copy of the aggregate schema.

## The decision, in one line The payload-shape decision comes down to how much a domain event should be a **self-contained snapshot** versus a **minimal, narrow signal** that something happened, and it has real consequences for coupling, staleness, and correctness that only show up once a system has more than one or two listeners. | | Thin event | Fat event | |---|---|---| | Payload | typically the aggregate's identifier, an occurred-at timestamp, and whatever fields are specific to that particular event | a broader snapshot embedded directly in the payload | ## The thin event A thin event carries the smallest set of data that lets a listener understand the fact and act on it: typically the aggregate's identifier, an occurred-at timestamp, and whatever fields are specific to that particular event — `OrderShipped` might carry `{orderId, trackingNumber, shippedAt}` and nothing else about the order. If a listener needs more, say the customer's email to send a shipping notification, it looks the `Order` or `Customer` back up via a repository at the time it handles the event, getting current state as of that moment. This keeps the event's shape stable and small: adding a new field to the `Order` aggregate doesn't require touching the `OrderShipped` event unless that field is relevant to shipping specifically, and the event can't leak data a listener wasn't supposed to see, because it was never included. ## The fat event, and what it costs A fat event instead embeds a broader snapshot directly in the payload, so a listener can act without any further lookup. This is attractive across a process or context boundary where a lookup means a network call, and is common practice at the integration-event layer, deliberately out of scope for the pure in-process domain-event decision here, where avoiding chatty cross-service calls matters a lot. Inside the domain model itself, though, a fat event has real costs. 1. **First, coupling:** every field included is now part of the event's contract, so any consumer relying on it makes that field harder to remove or rename later — the event's payload becomes a second, informally-versioned copy of the aggregate's shape that has to be kept in sync by hand. 2. **Second, staleness:** if a listener processes the event some time after it was raised — queued, retried, scheduled slightly later — the snapshot inside the event reflects the aggregate's state at the moment it was raised, not at the moment the listener runs; if the aggregate changed again in between, a listener that trusts the embedded snapshot instead of re-fetching can act on stale data without realizing it, whereas a thin event forcing a fresh lookup naturally avoids this. 3. **Third, over-exposure:** a fat snapshot handed to every listener means listeners that only care about one fact also receive fields they have no business seeing, both a minor privacy smell and something that makes it harder to reason about who depends on what. ## The failure mode that actually bites The failure mode that shows up most often in practice with fat events is exactly the staleness issue: a listener queued behind a slow downstream call processes an `OrderUpdated{full order snapshot}` event twenty seconds after it was raised, unaware that a second `OrderUpdated` event was raised and already processed by a faster listener in the meantime — if the slow listener naively writes its snapshot's values into a read-model or cache, it can overwrite newer data with older data, a classic out-of-order-update bug. Thin events don't eliminate this risk entirely, since ordering still matters for the delta itself, but they at least prevent a listener from confidently overwriting broad swaths of state based on a stale full snapshot; a listener working from a thin event and a fresh lookup only overwrites what it re-reads as current. ## Where many DDD codebases land A concrete, well-known resolution: many DDD codebases keep in-model domain events thin and intent-specific (`OrderShipped`, `PaymentCaptured`, each with a handful of directly relevant fields), and only build a fatter, versioned payload when translating a domain event into an outbound integration event at a bounded-context boundary — at which point the fatness is a deliberate contract decision made with API-versioning discipline, not an accidental byproduct of convenience inside the domain model itself.

  • Give a concrete scenario where a fat event's embedded snapshot leads to a listener acting on stale data.
    An OrderUpdated event with a full order snapshot is raised twice in quick succession because two fields changed close together; if a slower listener, perhaps behind a retry or backlog, processes the first event after a faster listener has already processed the second, and it writes its snapshot's values into a read-model without checking versioning, it silently overwrites newer state with older values. A thin event forcing a fresh database lookup at handling time would have read the current row and avoided the overwrite.
  • Why is embedding a full snapshot more acceptable for an integration event crossing a service boundary than for an in-process domain event?
    Across a service boundary, a listener that needs more context can't just do a cheap in-process repository lookup - it would mean a network call to another service, which is slower and can fail independently. That cost/benefit tips toward accepting the coupling and staleness risk of a fatter payload, especially when it's a deliberately versioned public contract rather than an internal implementation detail.
  • How does event payload staleness differ from event ordering problems, and can thin events fix ordering issues too?
    Staleness is about a listener trusting embedded data that's no longer current by the time it's read; ordering is about events arriving or being processed out of sequence. Thin events reduce staleness risk by forcing a fresh read, but they don't inherently fix ordering - a listener could still apply a delta from an older event after a newer one, so ordering typically needs its own mechanism, like a version number, regardless of payload thinness.

A thin event is like a text message saying 'left the office, headed your way' - short, specific, true at read time. A fat event is like forwarding your entire calendar for the day - convenient for the reader, but if plans change after you sent it, the calendar is now wrong and the reader doesn't know.

saying these in an interview costs you the question

  • Defaults to putting the entire aggregate's state into every domain event 'just in case'
  • Doesn't recognize that an embedded snapshot can be stale by the time a listener processes it
  • Assumes fat events and thin events have identical coupling implications
  • Can't explain why a listener acting on a stale snapshot might overwrite newer data
  • Treats domain-event payload shape and integration-event payload shape as the same design decision

context