skip to content

Why might a domain model raise a specific event like 'CustomerAddressChanged' or 'CustomerEmailVerified' instead of a single generic 'CustomerUpdated' event carrying a diff of changed fields? What's the trade-off?

level: seniorimportance: should knowfreq 55%

answer

  1. intent lives at the method that's called, not the diff
  2. fine-grained events = compile-time clarity, more types
  3. generic Updated event = cheap, pushes work to listeners
  4. fat generic event anti-pattern
  5. don't manufacture events nothing reacts to differently

basics

~20 s

Specific events tell listeners exactly what happened, so they can react directly without inspecting the data. A generic 'Updated' event is easier to add but forces every listener to figure out what actually changed by digging through the payload.

solid answer

~40 s

Fine-grained, intent-revealing events - CustomerAddressChanged, CustomerEmailVerified - encode the business meaning of a change directly in the type system, so a listener can subscribe to exactly the fact it cares about with compile-time safety. A single generic CustomerUpdated event with a field-diff payload is cheaper to introduce but pushes interpretation to runtime: every listener has to branch on which fields changed or string-match a change-type, which is fragile and loses information the aggregate already had (e.g. 'address changed because of relocation' vs 'because of a typo fix' collapse into the same diff). The trade-off is event-type proliferation and more wiring versus a single catch-all type that trades type safety and clarity for lower short-term cost.

go deeper

for a junior

Should understand at a high level that 'what happened' can be more specific than 'a field changed'.

for a middle

Should be able to name a concrete pair of operations where the same field-level change deserves two different event types.

for a senior

Should design event granularity deliberately, weighing type-proliferation cost against interpretation cost pushed to listeners, and recognize the fat-generic-event anti-pattern.

for a principal

Should set team-wide guidance on when to introduce a new event type versus reuse an existing one, and revisit that boundary as real consumers and their needs emerge.

## What "modeling intent" means "Modeling intent" in the domain-events context means designing events around the **business reason** something changed, not just around the technical fact that a field's value is now different. This matters because a database column changing value and a business-meaningful event happening are not the same thing, even though the former often triggers the latter. A customer's `shippingAddress` can change: - because they moved house; - because a support agent fixed a typo; - or because a fraud check overwrote it. Technically the same field mutation, but each is a different business event with different downstream consequences. `AddressCorrected` probably doesn't need to trigger "re-verify this customer's region for tax purposes"; `CustomerRelocated` probably does. ## Where the intent actually lives The mechanism for doing this well is to have the aggregate's business methods decide which event to raise, because the aggregate is the only place with direct access to **why** the change is happening — it's the caller's intent, expressed as which method was invoked. A method named `correctAddress(newAddress)` raises `AddressCorrected`; a method named `relocate(newAddress)` raises `CustomerRelocated`. Both might set the same underlying field, but the aggregate's API surface, not a diff of before/after state, determines the event. You can't reliably reconstruct intent-revealing events from outside the aggregate by comparing snapshots — the information about why only exists at the moment the business operation is invoked, and is lost once you're just looking at old-value/new-value. ## The generic alternative, and who pays for it The alternative — one generic `Updated` event per aggregate type, carrying the full new state or a diff — exists because it's **cheap**: one event type per aggregate, ever, and any new kind of change automatically produces an event without a developer defining a new type. Some teams deliberately choose this for aggregates with many rarely-differentiated fields, or as a stopgap early in a system's life when downstream consumers aren't known yet. The real trade-off is not correctness but where the cost of interpretation lands: | Approach | Who pays the interpretation cost | |---|---| | Intent-revealing events | paid upfront by whoever writes the aggregate: more design work, more types to maintain | | A generic `Updated` event | deferred to every future listener, each independently re-deriving "did the thing I care about actually change, and why" from a diff — duplicated work, easy to get subtly wrong | ## The fat generic event A concrete failure mode of the generic approach: as more listeners subscribe to `CustomerUpdated` for different reasons, the diff payload has to carry more fields "just in case", grows large and coupled to every field any consumer might want, and adding a genuinely new field risks silently triggering existing listeners that pattern-match loosely on the diff. This is the **"fat generic event" anti-pattern** — superficially decoupled, actually more tightly coupled, because every listener's correctness depends on the full shape of the diff rather than a narrow, stable contract. ## The opposite failure mode: sprawl The opposite failure mode is **event-type sprawl**: an aggregate raising a distinct event for every conceivable field mutation (`CustomerFirstNameChanged`, `CustomerLastNameChanged`, `CustomerPhoneChanged`...) when nothing downstream actually needs that granularity produces dozens of types with one listener or none, adding maintenance burden without adding clarity, because the "intent" being captured isn't actually business-meaningfully distinct. ## Where mature models land The practical resolution most mature domain models converge on: raise intent-revealing events at aggregate method boundaries where the business genuinely distinguishes cases, and don't manufacture separate event types for changes nothing downstream differentiates. In a billing domain, `SubscriptionUpgraded(planId)` and `SubscriptionDowngraded(planId)` are typically kept distinct from a generic `SubscriptionPlanChanged` because upgrade and downgrade trigger materially different consequences: - prorated billing direction; - feature-flag ordering; - dunning logic. So the extra type is worth its cost, whereas a field like marketing opt-in preference rarely needs its own dedicated event type beyond a generic `PreferencesUpdated`, because nothing currently reacts differently based on which preference changed.

  • Why can't you reliably reconstruct an intent-revealing event, like distinguishing 'address corrected' from 'customer relocated', just by diffing the aggregate's state before and after?
    Because both operations produce the exact same before/after diff on the address field - the information about why the change happened only exists at the moment the specific business method is invoked, as the caller's expressed intent. Once you're only looking at old and new values, that intent is already lost, so external diffing can at best guess.
  • What's a concrete symptom that a system has fallen into the 'fat generic event' anti-pattern?
    The event payload keeps growing to include more optional fields as new listeners are added, and existing listeners occasionally fire unintentionally because they loosely pattern-match on a diff or changeType field rather than subscribing to a narrow, stable event. You'll also see code like 'if changedFields contains X' scattered across many unrelated listeners, each re-deriving intent independently.
  • When is it reasonable to prefer a generic 'Updated' event over defining a new intent-revealing event type?
    When the different underlying causes of a change genuinely have no different downstream consequences yet, so a dedicated event type would be speculative design with no payoff. It's also reasonable as a deliberate stopgap early in a system's life when consumers aren't known yet, as long as the team revisits it once real listeners with differing needs appear.

It's like a nurse's chart note saying 'patient's temperature changed' versus 'fever spiked' or 'fever broke' - the raw number diff is the same shape either way, but only the intent-revealing note tells the next clinician what to actually do.

saying these in an interview costs you the question

  • Thinks event granularity should be derived by diffing old/new state rather than from the business method invoked
  • Defaults to one generic 'EntityUpdated' event for everything regardless of downstream needs
  • Can't give an example of two operations that mutate the same field but should raise different events
  • Creates a dedicated event type for every single field with no listener that needs it
  • Lets a generic event's payload keep growing to satisfy each new listener's ad hoc needs

context