A handler for domain events delivered through the outbox.
defmodule MyApp.OnOrderPlaced do
use DDD.EventHandler, events: [MyApp.Events.OrderPlaced]
@impl DDD.EventHandler
def handle(%MyApp.Events.OrderPlaced{} = event) do
case MyApp.Commands.ReserveStock.call(%{order_id: event.aggregate_id}) do
{:ok, _} -> :ok
{:error, error} -> {:error, error}
end
end
end
MyApp.OnOrderPlaced.subscribe()What handle/1 returns
:ok(or{:ok, _}, or any other non-error value): the event was handled.{:error, reason}: it depends on the kind of the error.- A business error (
%DDD.Error{kind: :business}, or any plain reason such as:not_found) means the handler looked at the event and said no. That outcome is deterministic, so it is recorded as rejected and never retried. It is not a dead letter. - An infrastructure error (
%DDD.Error{kind: :infrastructure}, for example a conflict or a failed transaction) is a temporary failure: the event is retried with a backoff and dead-lettered if it keeps failing.
- A business error (
- An exception, throw or crash is unexpected: retried like an infrastructure error.
In both failure cases a transactional handler is rolled back, so it never leaves half of its work behind.
Transactions and delivery guarantees
By default each delivery runs in its own database transaction together with a record in
event_deliveries that this handler processed this event. If the handler fails, everything
rolls back, including the record, and the event is retried later. If it succeeds, the handler
is never run again for that event, even when another handler of the same event failed and the
event is retried. For handlers that only write to our database that is effectively
exactly-once. Events a handler produces by saving aggregates commit with its work.
use DDD.EventHandler, events: [...] # transaction on the configured repo
use DDD.EventHandler, events: [...], repo: MyApp.Repo # another repo
use DDD.EventHandler, events: [...], transaction: false # no transactionA handler with an effect a transaction can't roll back (sending an email, calling an API)
should use transaction: false. It is then delivered at least once: if the server stops
after the effect but before the delivery is recorded, it runs again, so it must tolerate that.
Handlers that don't use DDD.EventHandler (any module with handle/1) still work and are
delivered like transaction: false handlers.
Summary
Functions
Deliver event (stored under event_id, the 16-byte outbox id) to handler.
Types
:delivered- the handler ran and succeeded:skipped- it had already processed (or rejected) this event{:rejected, reason}- a business error: recorded, never retried{:error, reason}- a failure to retry (infrastructure error, exception)