skip to content

In a system where placing an order should raise an OrderPlaced domain event, should the domain logic that decides to raise that event publish it directly to an event bus/message broker itself, or should it merely return/collect the event for the application service to publish after the transaction commits? Explain the reasoning.

level: principalimportance: nice to knowfreq 30%

answer

  1. events collected in domain, published in application layer
  2. publish-before-commit risk = phantom events
  3. dual-write problem
  4. transactional outbox pattern
  5. domain layer stays messaging-infrastructure-free

basics

~20 s

The business logic should just record that an event happened — not broadcast it. The application service should send it, and only after the database changes are safely committed, so nobody hears about something that later gets rolled back.

solid answer

~40 s

Domain logic (entities or domain services) should collect/return domain events as plain data rather than dispatch them to a message broker directly. The application service, after successfully committing the transaction, is responsible for actually publishing those events — often via a transactional outbox for reliability. This ordering matters for two reasons: publishing before commit risks announcing a change that later rolls back, so a subscriber reacts to something that never durably happened; and the domain layer shouldn't depend on messaging infrastructure at all, since that's an application/infrastructure concern. The outbox pattern closes the remaining gap (a crash between commit and publish) by writing the event into the same transaction as the state change, then relaying it separately with retries.

go deeper

for a junior

Can say events should be sent 'after saving' without necessarily explaining why.

for a middle

Can articulate that publishing before commit risks announcing a rolled-back change.

for a senior

Can describe the collect-then-publish pattern and name the dual-write problem as the underlying risk.

for a principal

Can weigh the transactional outbox pattern's reliability benefits against its operational cost, discuss idempotency implications for consumers, and judge when a simpler synchronous-publish-after-commit approach is an acceptable trade-off.

## Raising an event versus dispatching it This question sits right at the boundary between the application-service and domain-service split and a closely related concern: where domain events get raised versus where they get dispatched. The short answer is that domain logic should be responsible for deciding an event occurred, but not for transmitting it — and untangling why requires looking at both the dependency direction and the transactional-consistency problem. ## On dependency direction On dependency direction: domain services and entities are meant to be free of infrastructure concerns, and a message broker client is squarely infrastructure. If a domain service directly calls something like `messageBroker.publish(new OrderPlaced(orderId))`, the domain layer now has a hard dependency on your messaging technology — swapping brokers, or unit-testing the domain service, now requires either a real broker or a mock of one, which reintroduces exactly the infrastructure-testing cost the pure-domain-layer design was meant to avoid. The standard pattern instead has the aggregate or domain service simply record that an event occurred: - most commonly, the aggregate accumulates a private list of "uncommitted domain events" that a method like `pullDomainEvents()` returns and clears - or a domain service returns the event(s) as part of its result value Either way, an event object here is just a plain data structure (e.g., `OrderPlaced(orderId, placedAt)`), not something being transmitted — nothing about it references a broker, topic name, or serialization format. ## On transactional consistency On transactional consistency: this is the more operationally dangerous half of the reasoning. If a domain object published an event synchronously and immediately, before the surrounding transaction commits, and that transaction later rolls back — because a downstream save fails, a constraint violation triggers, or an exception is thrown two steps later in the same use case — then the event was already broadcast to the world describing a state change that never durably happened. A subscriber (say, a shipping-notification service or an analytics pipeline) receiving `OrderPlaced` would act on a phantom event, and there's no automatic way to unpublish it. This is a real, production-observed failure class often called the **dual-write problem**: you're writing to two different systems (the database and the event bus) and there's no way to make both writes atomic without extra machinery. The mitigation most production systems converge on: within the application service's single database transaction, the domain logic collects events but nothing is actually sent externally; only after the transaction successfully commits does the application service (or a dedicated event-dispatch component) actually publish. To make even that step reliable — because "commit succeeded, then the process crashes before publishing" is still a gap — many systems use the **transactional outbox pattern**: 1. the application service writes both the domain state change and a serialized representation of the event into an outbox table as part of the same database transaction 2. a separate poller/relay process reads unpublished outbox rows and sends them to the actual message broker, retrying until it succeeds 3. then marks them sent This closes the gap because the event's existence is now itself durably part of the same atomic write as the state change, and the actual broker dispatch can be retried independently without any duplicate business-state mutation. ## The trade-off worth naming The trade-off worth naming explicitly: outbox-pattern reliability adds real operational cost — - an extra table - a relay/poller process to operate and monitor - at-least-once delivery semantics that push idempotency requirements onto every consumer, since a crash between "sent to broker" and "marked sent in outbox" can cause a redelivery Teams building a low-stakes internal admin tool where an occasional missed or duplicate notification is a shrug, not an incident, may reasonably skip the outbox and just publish synchronously right after commit, accepting the small window of risk between commit and publish. The decision is a genuine cost/reliability trade-off, not a universal "always use outbox" rule — it depends on what breaks if a downstream consumer misses or double-processes an event. ## A concrete illustration A concrete illustration: an e-commerce checkout flow where `OrderPlaced` triggers both an inventory-reservation service and a customer-facing confirmation email. If the event fired before commit and the transaction then rolled back (say, a payment authorization failure discovered two steps later), inventory could be reserved and a confirmation email sent for an order that doesn't exist in the database — a directly customer-visible bug. Structuring the domain layer to only collect the event, and having the application service publish, ideally via an outbox, strictly after a successful commit, is what prevents that class of bug by construction rather than by discipline alone.

  • What goes wrong if you publish the domain event synchronously inside the same method as the aggregate mutation, but before the application service's transaction commits?
    If the transaction later rolls back for any reason downstream in the same use case, subscribers have already reacted to a change that never became durable, which can cascade into visible bugs like sending a confirmation email or reserving inventory for an order that doesn't actually exist in the database.
  • Why does the outbox pattern require a separate relay/poller process instead of just publishing directly in the same transaction?
    Because a message broker generally isn't part of the same transactional resource as the database, so you can't atomically commit a database row and a broker message together; writing the event to an outbox table inside the database transaction makes its existence atomic with the state change, and a separate process then handles the inherently non-transactional step of actually delivering it to the broker, retrying safely since delivery is decoupled from the original write.
  • Does the outbox pattern's at-least-once delivery guarantee mean event consumers can assume they'll only see each event once?
    No — at-least-once means duplicates are possible (e.g., if the relay crashes after sending but before marking the outbox row sent), so consumers need to handle events idempotently, typically by tracking processed event IDs and ignoring repeats, rather than assuming exactly-once delivery.

It's like a courtroom stenographer noting 'the verdict is guilty' during deliberation, versus the court clerk who only mails out the official notice after the judge's ruling is finalized and signed — you record the fact as it happens, but you don't tell the outside world until it's truly final and can't be reversed.

saying these in an interview costs you the question

  • Thinks domain services should call a message broker directly
  • Doesn't see a problem with publishing before the transaction commits
  • Unaware of the dual-write problem or thinks it's purely theoretical
  • Assumes outbox-pattern delivery is exactly-once with no idempotency needed downstream

context