It was an elegant “Eventsourcing and CQRS in Elixir” talk given by @vasspilka at ElixirConf EU 2021. It shows how to use the Commanded library by @slashdotdash to model a Bank domain with the Domain-Driven Design approach and make an event-driven system out of it.
Having the well-defined model with Command and Event structs, it still can be an issue to validate data coming from the User Interface and validate the consistency of casting commands and events in each other.
For example, the value of the bank account identifier should be both of binary type and have a specific format. That is different from other kinds of binary identifiers across the system.
One of the possible ways to solve the validation problems defined above is by using Domo library in Command and Event structs like that:
# defining the format precondition for value's type
defmodule Bank do
import Domo
@type account_number() :: binary()
precond account_number: &validate_account_number/1
def validate_account_number(string) do
if string =~ ~r/^[[:digit:]]{3}-[[:digit:]]{3}$/, do: :ok, else: {:error, "Account number should be of xxx-xxx, x = 0..9 format."}
end
...
end
# using Domo in the event struct
defmodule Bank.Core.Commands.DepositMoney do
use Domo, ensure_struct_defaults: true
...
# automatically adds new_ok/1 and new!/1 constructors
end
# using constructor in the Accounts context
defmodule Bank.Core.Accounts do
def deposit_money(acc_id, amount) do
[account_id: acc_id, amount: amount]
|> DepositMoney.new_ok()
|> maybe_dispatch(returning: :execution_result)
end
defp maybe_dispatch({:ok, command}, opts), do: Bank.Core.Application.dispatch(command, opts)
defp maybe_dispatch({:error, _message} = error, _opts), do: error
...
end
Then the following errors can be handled automatically:
iex(1)> Accounts.deposit_money(nil, 10)
{:error,
[
account_id: "Invalid value nil for field :account_id of %Bank.Core.Commands.DepositMoney{}. Expected the value matching the <<_::_*8>> type."
]}
iex(2)> Accounts.deposit_money("100102", 500)
{:error,
[
account_id: "Account number should be of xxx-xxx, x = 0..9 format."
]}
iex(3)> Accounts.deposit_money("100-102", -12)
{:error,
[
amount: "Invalid value -12 for field :amount of %Bank.Core.Commands.DepositMoney{}. Expected the value matching the non_neg_integer() type."
]}
iex(4)> Accounts.deposit_money("100-102", 120)
{:ok, %Commanded.Commands.ExecutionResult{}}
Domo makes it possible to associate a precondition (format validation) function with the given @type
definition and adds new_ok/1
validating constructor and several others to a struct.
The consistency of commands and events is ensured with the call to the validating constructor.
See the complete Bank example from the @vasspilka’s talk seasoned with Domo on GitHub - IvanRublev/bank-commanded-domo.
More details about Domo Domo v1.3.2 — Documentation