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
| Term | Meaning |
|---|---|
| Sync ACL | Request/response Anti-Corruption Layer (local port → adapter → peer façade) |
| Translation listener | Consumer 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
| Concern | Rule |
|---|---|
| At-least-once | Assume duplicates; never assume exactly-once from the bus |
| Idempotency | Inbound Use Case (or a dedicated Infra gate before it) must tolerate the same eventId twice |
| Ordering | Do not assume global order across aggregate types; design consumers for out-of-order when possible |
| Failure | Failed handling retries; poison messages need host dead-letter / alert (adapter concern) |
Required payload fields (minimum)
On every cross-module Domain Event (or its envelope):
eventId— unique per occurrence (UUID)occurredAt— instant the domain fact happened (via SharedClockInterfaceat publish time)schemaVersion— contract version for this event type- 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.
Async / messaging (consumer Infra → inbound Use Case)
| Host | Typical place |
|---|---|
| Laravel | Queued listeners / jobs + Shared dispatcher port |
| Symfony | Messenger handlers in Infrastructure |
| Yii | Queue workers / handlers in Infrastructure |
| CodeIgniter | Commands / queue consumers in Infrastructure |
| CakePHP | Events / queue consumers in Infrastructure |
| Spiral | RoadRunner / queue jobs in Infrastructure |
| Slim | Bring-your-own bus/worker — still Infra translation |
| Mezzio | Bring-your-own bus/worker — still Infra translation |
Full topic pages: Adapters overview
When a query port is allowed
| Pattern | Allowed? |
|---|---|
| Happy path: thin event with only an id; listener calls publisher Domain/Use Case | No |
| Happy path: rich event → inbound Use Case | Yes (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 Application | No |
Anti-patterns
| Smell | Fix |
|---|---|
| “We’ll just query Ordering every time” | Enrich the event |
| Idempotency only in the queue framework | Persist eventId in the consumer |
| Swallowing failures silently | Retry + dead-letter + alert |
| Putting bus SDKs in Domain | Shared dispatcher port + Infra adapter |
Next: orchestration · transactions.