A simplified set of tools for Event Sourcing.

While there are many technical and business benefits to Event Sourcing, proper implementation tends to require understanding of many new concepts and technical details. This package aims to make implementation easier and more enjoyable by providing a small extensible API with few dependencies baked in.

Installation

If available in Hex, the package can be installed by adding must to your list of dependencies in mix.exs:

def deps do
  [
    {:must, "~> 0.1.0"}
  ]
end

Interface

  • Must.process_command/2: a unified function for processing a command.
  • Must.Command: an extensible protocol for processing commands.
  • Must.Event: an extensible protocol for processing events.
  • Must.Storage: a behaviour for storing events.

When prototyping a system or testing Must's fitness, it may be unnecessary to fully implement the Must.Command and Must.Event protocols. Both protocols have a @fallback_to_any true directive, so it is possible to define a fallback implementation using Any.

For example, authorization may be bypassed by defining a fallback implementation that returns the command as-is:

defimpl Must.Command, for: Any do
  def be_authorized!(command, _opts), do: command
end

Event Persistence

Several adapters are planned to support different persistence strategies:

Each adapter will need to:

  • Initialize a standardized data structure (see cloudevents spec)
  • Persist events to a storage backend
  • Handle event persistence errors
  • Provide a way to query events from the storage backend
  • Track the last seen event version
  • Handle event version conflicts
  • Support testing

Event Delivery

Several delivery mechanisms are planned to support different event delivery strategies:

Examples

Many systems have a process for activating a user .

defmodule ActivateUser do
  @moduledoc "Command for activating a user."
  use Ecto.Schema

  @primary_key false
  embedded_schema do
    field :user_id, :integer
  end

  def changeset(%__MODULE__{} = command, params) do
    fields = __MODULE__.__schema__(:fields)

    command
    |> Ecto.Changeset.cast(params, fields)
    |> Ecto.Changeset.validate_required(fields)
  end

  defimpl Must.Command do
    def be_authorized!(%__MODULE__{} = command, opts) do
      actor = Keyword.fetch!(opts, :actor)
      actor.user_id != command.user_id
      actor.organization.status == :active
      actor.organization.role in [:admin, :manager]
    end

    def be_valid!(command, opts) do
      params = Keyword.fetch!(opts, :params)

      command
      |> changeset(params)
      |> Ecto.Changeset.apply_action!(:validate)
    end

    def be_translated_to_events!(command, opts) do
      metadata = Keyword.get(opts, :metadata, %{})
      
      [
        %UserActivated{user_id: command.user_id, metadata: metadata}
      ]
    end
  end
end

The example above demonstrates:

  • How to define a command struct and changeset
  • How to implement the Must.Command protocol

Colocation

While Must.Command is implemented directly in the ActivateUser example, it is also possible to define implementations elsewhere. Having the command and its rules in one place may aid developers and LLMs to understand the behavior while minimizing context switching.

However, this is not a requirement. Some teams may prefer to consolidate implementations into a separate module/file, for example.

The simplest way to process a command is to use the Must.process_command/2 function, which takes a command struct and a keyword list of options. The option keys are determined by the Must.Command implementation.

%ActivateUser{}
|> Must.process_command(
  params: %{"user_id" => 123},
  metadata: %{"actor" => current_user}
)

If the command is processed successfully, a list of events will be returned:

[
  %UserActivated{
    user_id: 123,
    metadata: %{"actor_id" => 1, "timestamp" => ~U[2026-01-01 01:00:00.123456Z]}
  }
]

Design Decisions

To support a wide variety of use cases, the Must protocols may be implemented for structs or plain maps. For most systems, it is recommended to define commands as structs to provide clear intent to developers and coding tools. This approach also allows authorization, validation, and handling to be implemented close to the command definition. Readers can view a single file to understand the command definition and its behavior.

For best results, return the command struct if all conditions are met, or raise an error if any conditions are not met

Each protocol accepts two arguments: a struct/map and options. The protocol is intentionally agnostic about what data is passed as options. Some implementations may options as keyword lists, while others may use a struct/map. It is recommended to establish follow consistent patterns for each protocol implenentation to support effictient development and maintenance.

What Abouts

Experienced Event Sourcing developers may be wondering where several typical components and concerns are defined in this package.

  • Projections
  • Process Managers
  • Value Objects
  • Contexts
  • Aggregates
  • Dynamic consistency boundaries
  • Snapshots

Must aims to empower engineers to be productive quickly, with or without prior Event Sourcing experience. The value of Event Sourcing is in its state management and reactivity properties, not in its jargon. With a simpler approach, the hope is to make Event Sourcing accessible to a wider audience. Technicians and leaders who are apprehensive about adopting Event Sourcing may find Must to be a more approachable alternative to implementations which strictly adhere to the academic concepts.

While the interface is simple, all of the traditional Event Sourcing concepts may be supported through Must's extensible design.