X3m.System

Build Status Hex version Coverage Status

Building blocks for distributed and/or CQRS/ES systems in Elixir.

X3m.System gives you a small set of composable pieces for building message-driven backends: a message that carries a request and its response, a router that registers services across a cluster, a dispatcher that finds a node offering a service and waits for the reply, and — when you need it — aggregates with event sourcing and a backend-agnostic scheduler for delivering messages in the future.

The pieces are à la carte. You can use the messaging layer (message + router + dispatcher) on its own, add aggregates and event sourcing only where you need them, and use the scheduler independently of everything else.

Installation

def deps do
  [
    {:x3m_system, "~> 0.10.0"}
  ]
end

A minimal example

Define a router that registers a service and the module that handles it, and register the services (typically from your application's start/2).

defmodule MyApp.Router do
  use X3m.System.Router

  service :greet, MyApp.Greeter

  def authorize(_message), do: :ok
end

defmodule MyApp.Greeter do
  alias X3m.System.Message

  def greet(%Message{} = message) do
    name = message.raw_request["name"]
    {:reply, Message.ok(message, "Hello, #{name}!")}
  end
end

:ok = MyApp.Router.register_services()

Dispatch a message to the service by name:

:greet
|> X3m.System.Message.new(raw_request: %{"name" => "Ada"})
|> X3m.System.Dispatcher.dispatch()
#=> %X3m.System.Message{response: {:ok, "Hello, Ada!"}, ...}
flowchart LR
  C[Caller] -->|"Message.new(:greet)"| D[Dispatcher.dispatch]
  D -->|find a node offering :greet| R[Router]
  R -->|"authorize/1"| A{authorized?}
  A -->|no| F["response: {:error, :forbidden}"]
  A -->|yes| H["Greeter.greet/1"]
  H -->|"{:reply, Message.ok(...)}"| C

No aggregates or event store are involved here — any module registered through a router can be a dispatch target.

Dispatch accross the cluster

If you want to try dispatch accross the cluster, run 2 iex sessions:

iex --sname x3m_1@localhost -S mix

and from the other terminal

iex --sname x3m_2@localhost -S mix

Define your router and message handler (Greeter here) and register your services in first iex session (like in example from above), and then from the second one, connect to the first node:

Node.connect :"x3m_1@localhost"
#=> true

... and then dispatch message the same way you did in previous example. Result will be the same.

Guides

Example

The Banking example is a complete poncho project demonstrating the full CQRS/ES flow — HTTP API, command aggregates, event store, listener-driven read model, and cross-node dispatch. See its README for setup and curl walkthrough.

Releasing

  1. On a branch, bump @version in mix.exs and the README install snippet, and rename the CHANGELOG # Unreleased heading to the new version; merge the PR into main.
  2. Publish a GitHub Release whose new tag is the bare version (X.Y.Z, no v) targeting main, e.g. gh release create X.Y.Z --target main.
  3. The release workflow checks that the tag matches mix.exs and is on main, runs mix bless and publishes the package and docs to hex. It needs the repository secret HEX_API_KEY, a hex key scoped to the x3m_system package. Prereleases are not published until promoted.

License

Released under the MIT License. See the LICENSE file.