Event handling

Domain events

Domain events indicate that something of importance has occurred, within the context of an aggregate. They are named in the past tense: account registered; funds transferred; fraudulent activity detected.

Create a module per domain event and define the fields with defstruct. An event should contain a field to uniquely identify the aggregate instance (e.g. account_number).

defmodule BankAccountOpened do
  defstruct [:account_number, :initial_balance]
end

Note, due to event serialization you should expect that only: strings, numbers and boolean values defined in an event are preserved; any other value will be converted to a string. You can control this behaviour as described in serialization section.

Event handlers

Use the Commanded.Event.Handler macro within your event handler module to implement the defined behaviour. This consists of a single handle/2 function that receives each published domain event and its metadata, including the event’s unique event number. It should return :ok on success or {:error, :reason} on failure. You can return {:error, :already_seen_event} to skip events that have already been handled, due to the at-least-once event delivery of the supported event stores.

Use pattern matching to match on each type of event you are interested in. A catch-all handle/2 function is included, so all other events will be ignored by default.

defmodule AccountBalanceHandler do
  use Commanded.Event.Handler, name: "account_balance"

  def init do
    with {:ok, _} <- Agent.start_link(fn -> 0 end, name: AccountBalance) do
      :ok
    end
  end

  def handle(%BankAccountOpened{initial_balance: initial_balance}, _metadata) do
    Agent.update(AccountBalance, fn _ -> initial_balance end)
  end

  def handle(%MoneyDeposited{balance: balance}, _metadata) do
    Agent.update(AccountBalance, fn _ -> balance end)
  end

  def current_balance do
    Agent.get(AccountBalance, fn balance -> balance end)
  end
end

The name given to the event handler must be unique and remain unchanged between releases. It is used when subscribing to the event store to track which events the handler has seen during restarts.

{:ok, _handler} = AccountBalanceHandler.start_link()

You can choose to start the event handler’s event store subscription from the :origin, :current position or an exact event number using the start_from option. The default is to use the origin so your handler will receive all events.

# start from :origin, :current, or an explicit event number (e.g. 1234)
defmodule AccountBalanceHandler do
  use Commanded.Event.Handler, name: "account_balance", start_from: :origin

  # ...
end

# You can optionally override :start_from by passing it as param
{:ok, _handler} = AccountBalanceHandler.start_link(start_from: :current)

Use the :current position when you don’t want newly created event handlers to go through all previous events. An example would be adding an event handler to send transactional emails to an already deployed system containing many historical events.

You should start your event handlers using a supervisor to ensure they are restarted on error.

Metadata

The handle/2 function in your handler receives the domain event and a map of metadata associated with that event. You can provide the metadata key/value pairs when dispatching a command:

:ok = ExampleRouter.dispatch(command, metadata: %{"issuer_id" => issuer_id, "user_id" => "user@example.com"})

In addition to the metadata key/values you provide, the following system values will be included in the metadata passed to an event handler:

  • event_id - a globally unique UUID to identify the event.
  • event_number - a globally unique, monotonically incrementing and gapless integer used to order the event amongst all events.
  • stream_id - the stream identity for the event.
  • stream_version - the version of the stream for the event.
  • causation_id - an optional UUID identifier used to identify which command caused the event.
  • correlation_id - an optional UUID identifier used to correlate related commands/events.
  • created_at - the date/time, in UTC, indicating when the event was created.

These key/value metadata pairs will use atom keys to differentiate them from the user provided metadata:

defmodule ExampleHandler do
  use Commanded.Event.Handler, name: "ExampleHandler"

  def handle(event, metadata) do
    IO.inspect(metadata)
    # %{
    #   :causation_id => "db1ebd30-7d3c-40f7-87cd-12cd9966df32",
    #   :correlation_id => "1599630b-9c38-433c-9548-0dd793108ba0",
    #   :created_at => ~N[2017-10-30 11:19:56.178901],
    #   :event_id => "5e4a0f38-385b-4d57-823b-a1bcf705b7bb",
    #   :event_number => 12345,
    #   :stream_id => "e42a588d-2cda-4314-a471-5d008cce01fc",
    #   :stream_version => 1,
    #   "issuer_id" => "0768d69a-d2b7-48f4-d0e9-083a97f7ebe0",
    #   "user_id" => "user@example.com"
    # }

    :ok
  end
end

Consistency guarantee

You can specify an event handler’s consistency guarantee using the consistency option:

defmodule AccountBalanceHandler do
  use Commanded.Event.Handler,
    name: "account_balance",
    consistency: :eventual

The available options are :eventual (default) and :strong:

  • Strong consistency offers up-to-date data but at the cost of high latency.
  • Eventual consistency offers low latency but read model queries may reply with stale data since they may not have processed the persisted events.

You request the consistency guarantee, either :strong or :eventual, when dispatching a command. Strong consistency will block the command dispatch and wait for all strongly consistent event handlers to successfully process all events created by the command. Whereas eventual consistency will immediately return after command dispatch, without waiting for any event handlers, even those configured for strong consistency.