Trogon.Dispatcher
Trogon.Dispatcher is a compile-time command and query dispatcher for Elixir. In-process, stateless, and synchronous: no processes, no persistence, and no opinion about your domain.
It gives a host app a single MyApp.Dispatcher.dispatch_message/2 entry point, built at compile time. Commands and queries are structs registered with register_message, and handlers are plain modules exporting handle_message/2. Middleware wraps the pipeline Rack style and deals only in contexts, dispatchers compose into larger dispatchers with import_dispatcher, and every dispatch emits a :telemetry span.
Code that routes messages to handlers tends to end up either coupled to an event sourcing framework or scattered across ad-hoc case statements. This library owns the cross-cutting concerns, meaning routing, middleware composition, observability and test tooling, and owns no architecture: there are no events, no aggregates, and no repos. Routing resolves at compile time into direct function clauses, so a duplicate registration, a missing handler, or a dispatcher cycle fails the build rather than a request. Dispatching an unregistered struct returns an error. Every dispatch is a :telemetry span, and an optional Trogon.Dispatcher.OpenTelemetry integration turns it into a trace; see Trace with OpenTelemetry.
It is for Elixir teams that want an explicit, observable boundary between their web or worker layer and their domain code. That suits applications which have outgrown scattered routing but do not want to take on a framework's opinion about how the domain itself is implemented, and teams who need the dispatch boundary to be the one place identity propagation, authorization and tracing are applied.
Usage
defmodule MyApp.Accounts.RegisterUser do
@behaviour Trogon.Dispatcher.Handler
defstruct [:email]
@impl true
def handle_message(%__MODULE__{} = message, _context) do
{:ok, %MyApp.Accounts.User{email: message.email}}
end
end
defmodule MyApp.Accounts.GetUser do
@behaviour Trogon.Dispatcher.Handler
defstruct [:id]
@impl true
def handle_message(%__MODULE__{} = message, _context) do
{:ok, MyApp.Accounts.fetch_user!(message.id)}
end
end
defmodule MyApp.Accounts.Dispatcher do
use Trogon.Dispatcher
middleware MyApp.Accounts.RequireTenant
register_message MyApp.Accounts.RegisterUser, kind: :command
register_message MyApp.Accounts.GetUser, kind: :query
end
defmodule MyApp.Dispatcher do
use Trogon.Dispatcher
middleware MyApp.Authorize
import_dispatcher MyApp.Accounts.Dispatcher
import_dispatcher MyApp.Billing.Dispatcher
end
iex> MyApp.Dispatcher.dispatch_message(%MyApp.Accounts.RegisterUser{email: "a@b.c"})
{:ok, %MyApp.Accounts.User{email: "a@b.c"}}
Documentation
Tutorials
How-to
- Compose dispatchers
- Write a middleware
- Test with Mox
- Observe with telemetry
- Trace with OpenTelemetry