Cross-module events (async)
Purpose: when module A finished work and module B must react later, without returning a value into A’s Use Case.
Illustrative example: Ordering order fulfilled → Warehouse deducts reserved stock.
Operational detail (idempotency, outbox, duplicates): Event delivery. Contract shape: Cross-module contracts.
High-level shape
Critical boundary: only the translation listener (Infrastructure) may import the publisher’s event class. Consumer Application/Domain stay publisher-agnostic.
Naming: older text may say “ACL listener.” That means the same Infrastructure translation boundary — not a sync ACL port.
Sequence — fulfill then deduct
Payload rule (happy path)
Publish a rich event: everything the consumer needs for the normal reaction, plus eventId, occurredAt, and schemaVersion.
Do not publish only an ID and force the consumer to call back into Ordering Domain on every message.
Allowed later: a separate consumer-owned query/ACL port for corrections or rebuilds — see Event delivery. That must not replace rich events on the happy path.
Layer placement
| Piece | Module / layer | Notes |
|---|---|---|
| Event class | Publisher Domain | Pure PHP; no framework serialization traits on the Domain event |
| Dispatch | Publisher Use Case via Shared port | Not a framework facade inside the Use Case |
| Translation listener | Consumer Infrastructure | Maps foreign event → local DTO; may record eventId |
| Inbound Use Case | Consumer Application | Same rules as any Use Case |
ACL vs Event
| Situation | Bridge |
|---|---|
| Check stock while placing the order | ACL |
| After fulfillment succeeded, deduct reserved stock | Event |
| Validate member tier before confirming | ACL |
| Notify another system that shipment was created | Event |
| Reserve → pay → confirm across modules | Orchestration (docs) |
Replacing the publisher with HTTP / webhooks
Anti-patterns
| Anti-pattern | Fix |
|---|---|
| Listener calls publisher Use Case / repository | Map from event payload (or dedicated correction ACL) |
| Domain Event carries ORM models | Use primitives / Domain DTOs |
Consumer Application imports OrderFulfilled | Keep import in Infrastructure listener only |
| Empty event + “consumer will query Ordering Domain” | Enrich the event |
| Synchronous dispatch hiding a required ACL | If you need the result, use ACL |
No idempotency on eventId | Event delivery |
Next: event delivery · shared kernel · decision tree.