4 min readCalm pace · scan the outline anytime

Event delivery

Idempotency, duplicates, correlation, outbox, and when a query port is allowed after Domain Events.

Event delivery

Async bridges are eventually consistent. Core does not pick a queue vendor; it defines behaviours every host must honour.

Related: Cross-module events · Contracts · Transactions.

Terminology

TermMeaning
Sync ACLRequest/response Anti-Corruption Layer (local port → adapter → peer façade)
Translation listenerConsumer Infrastructure handler that maps a foreign Domain Event (or webhook) → local Application DTO → inbound Use Case

Calling the listener an “ACL listener” is informal shorthand for “translation at the boundary.” Prefer translation listener in new docs; both mean Infrastructure-only import of the publisher type.

Delivery expectations

ConcernRule
At-least-onceAssume duplicates; never assume exactly-once from the bus
IdempotencyInbound Use Case (or a dedicated Infra gate before it) must tolerate the same eventId twice
OrderingDo not assume global order across aggregate types; design consumers for out-of-order when possible
FailureFailed handling retries; poison messages need host dead-letter / alert (adapter concern)

Required payload fields (minimum)

On every cross-module Domain Event (or its envelope):

  1. eventId — unique per occurrence (UUID)
  2. occurredAt — instant the domain fact happened (via Shared ClockInterface at publish time)
  3. schemaVersion — contract version for this event type
  4. Business payload — fields the consumer needs for the happy path (rich enough to avoid Domain re-fetch)

Optional but recommended: correlationId, causationId, publisher aggregate id/code.

Idempotency pattern

Store processed eventIds in the consumer module (Infrastructure table or equivalent). Application/Domain stay free of bus APIs.

Outbox (host adapter)

Prefer: persist aggregate change + outbox row in one publisher transaction; a host worker publishes to the bus afterward. That prevents “Domain committed but event never left.” Concrete outbox tables/jobs are adapter details.

Illustrative host mappings — not Core MUST

Async / messaging (consumer Infra → inbound Use Case)

HostTypical place
LaravelQueued listeners / jobs + Shared dispatcher port
SymfonyMessenger handlers in Infrastructure
YiiQueue workers / handlers in Infrastructure
CodeIgniterCommands / queue consumers in Infrastructure
CakePHPEvents / queue consumers in Infrastructure
SpiralRoadRunner / queue jobs in Infrastructure
SlimBring-your-own bus/worker — still Infra translation
MezzioBring-your-own bus/worker — still Infra translation

Full topic pages: Adapters overview

When a query port is allowed

PatternAllowed?
Happy path: thin event with only an id; listener calls publisher Domain/Use CaseNo
Happy path: rich event → inbound Use CaseYes (default)
Correction / rebuild / missing optional field: consumer sync port to a thin provider façade (HTTP later)Yes, as a separate capability — not a substitute for rich events
Listener imports publisher ApplicationNo

Anti-patterns

SmellFix
“We’ll just query Ordering every time”Enrich the event
Idempotency only in the queue frameworkPersist eventId in the consumer
Swallowing failures silentlyRetry + dead-letter + alert
Putting bus SDKs in DomainShared dispatcher port + Infra adapter

Next: orchestration · transactions.

Modular Hexagonal Domain-Driven Design
Core 1.0.0-draft