Commerce services emit typed events after successful state changes. Listen for side effects; do not use listeners to repair core invariants that belong in the transaction.
Create a listener
import { OrderPlaced } from "@adocommercekit/core/events";
import type { EventsList } from "@adonisjs/core/types";
export default class SendOrderReceipt {
async handle(event: InstanceType<typeof OrderPlaced>) {
// Dispatch an idempotent mail job keyed by event.orderId.
}
}Register it using the Adonis emitter:
// start/events.ts
import emitter from "@adonisjs/core/services/emitter";
emitter.on("commerce.order.placed", [
() => import("#listeners/send_order_receipt"),
"handle",
]);Use the event class’s exported name rather than copying a string from logs when possible.
Inline or outbox delivery
With events.outbox.enabled: false, Adocommerce Kit emits after transaction success in the current process. This is simple but a crash after commit can lose the side effect.
With outbox mode enabled, event envelopes are written in the same transaction as commerce state. DrainEventOutbox claims pending rows, delivers them, and retries with backoff. Consumers must still be idempotent because at-least-once delivery may repeat a handler.
Listener rules
- Use aggregate IDs from the event and reload current state.
- Key jobs and external writes by event identity or aggregate transition.
- Keep listeners small; dispatch slow work through
JobDispatcher. - Never throw to roll back a transaction that has already committed.
- Treat event payloads as public contracts. Add fields compatibly; do not repurpose existing fields.
- Avoid listener chains where event A writes core state solely to trigger event B. Put orchestration in a service or workflow.
For audit or analytics consumers, store the event name, aggregate ID, and observed timestamp. Do not persist raw customer PII unless the downstream system has an explicit retention policy.