No description
Find a file
Rosa Richter 29eabbcf3a
All checks were successful
ci/woodpecker/push/test Pipeline was successful
feat(ci): Add dependency check cron job
2026-06-03 14:27:03 -06:00
.woodpecker feat(ci): Add dependency check cron job 2026-06-03 14:27:03 -06:00
lib chore: Remove unused Logger references 2026-06-03 14:26:40 -06:00
test chore: add ex_check 2025-10-24 17:54:44 -06:00
.formatter.exs Initial commit 2024-10-25 10:47:35 -06:00
.gitignore Add dialyzer and start typespecs 2024-10-25 13:43:04 -06:00
LICENSE.md Add a README and LICENSE 2024-10-25 12:19:05 -06:00
mise.toml chore: Update dependency Elixir to 1.20 2026-06-03 14:27:01 -06:00
mix.exs fix(deps): override decimal version to 3.1.0 2026-05-13 10:42:11 -06:00
mix.lock chore(deps): update dependency ex_doc to v0.40.3 2026-05-28 12:07:20 +00:00
README.md fix(docs): update CI badge [ci skip] 2026-05-27 11:35:55 -06:00
renovate.json Update renovate.json 2026-05-27 16:03:54 +00:00

Tragedy

status-badge

Tragedy is a framework for event-sourced applications, inspired heavily by Commanded.

Define your domain using the protocols and behaviors provided, then you can start dispatching events to Tragedy where they will update aggregates and publish events.

Warning

This project is called Tragedy because that's what will happen if you try to use this in production. It is currently very experimental.

Usage

Tragedy organizes processes into three categories: Aggregates, Sagas, and Listeners.

  • Aggregates form the core of your business logic, receiving commands from your application, and producing events that describe what actions take place.
  • Sagas receive events and can issue commands of their own, so that you can coordinate aggregates and implement complex logic with steps that can fail.
  • Listeners also receive events and allow you to plug in arbitrary business logic, like read model projections.

First, define an aggregate with Tragedy:

defmodule MyApp.BankAccount do
  use Tragedy, :aggregate

  defstruct [
    :account_id,
    :balance
  ]

  def new(id) do
    %__MODULE__{account_id: id}
  end

  defimpl Tragedy.Aggregate do
    def id(account) do
      account.account_id
    end

    def execute(%{account_id: account_id}, %MyApp.CreateAccount{account_id: account_id}) do
      %MyApp.AccountCreated{account_id: account_id}
    end

    def apply(account, %MyApp.AccountCreated{}) do
      %{account | balance: 0}
    end
  end
end

Then, you must define a command that will affect that aggregate, as well as one or more events that indicate what the command caused to happen:

defmodule MyApp.CreateAccount do
  @derive {Tragedy.Command, mod: MyApp.BankAccount, key: :account_id}
  defstruct [
    :account_id
  ]
end

defmodule MyApp.AccountCreated do
  defstruct [
    :account_id
  ]
end

Once you have these data structures, you can hand them to Tragedy to be run.

{:ok, pid} = Tragedy.DomainSupervisor.start_link(%Tragedy.DomainConfig{})

:ok = Tragedy.DomainSupervisor.dispatch(pid, %MyApp.CreateAccount{account_id: "1"})

When this CreateAccount command is handed to the domain supervisor, Tragedy will:

  1. create a new GenServer process for the BankAccount aggregate with account_id: "1"
  2. execute the command in the new aggregate, which produces an AccountCreated event
  3. update the aggregate using the event

Since we don't have any sagas or listeners, that's the end of this story. See the HexDocs for information on how to create those.

Architecture

Aggregates, sagas, and listeners all run in separate processes, and handle events concurrently, each category under its own supervisor.

flowchart TD
    DomainSupervisor
    DomainSupervisor --> AggregatePoolSupervisor
    DomainSupervisor --> SagaSupervisor
    DomainSupervisor --> ListenerSupervisor
    DomainSupervisor --> CommandRouter
    AggregatePoolSupervisor -->|1..n| AggregateServer
    SagaSupervisor -->|1..j| SagaServer
    ListenerSupervisor -->|1..k| ListenerServer

Installation

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

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

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/tragedy.

License

Copyright © 2024 Rosa Richter

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.