Configuring Commanded for a single-node
This post (series?) is intended to walk the reader through installing, configuring Commanded for use in a single-node Phoenix web app backed by Postgres. Commanded is a CQRS/Event Sourcing library for Elixir. It can use Postgres as a backing event store or it can use “Event Store”. It can be used in single-node as well as in clustered configuration.
Commanded by default wants you to use a separate database for it’s work. That means that sharing the same database as your Repo
isn’t going to work out of the box. We’re sticking with the defaults here and will setup a separate database for Commanded.
Create the web application
I’ve got my tools setup, so now I’ll create my Phoeonix project. The binary_id
option makes Ecto choose UUIDs for primary and foreign keys instead of integers by default, which is handy for Commanded.
$ mix phx.new coffee_shop --binary-id
* creating coffee_shop/config/config.exs
* creating coffee_shop/config/dev.exs
* creating coffee_shop/config/prod.exs
...
Create the database
$ cd coffee_shop
$ mix ecto.create
Compiling 13 files (.ex)
Generated coffee_shop app
The database for CoffeeShop.Repo has been created
It might work out for you as it did for me here, but it’s also possible that your database configuration isn’t quite right. Have a look at config/dev.exs and tweak the values there to match your setup:
use Mix.Config
# Configure your database
config :coffee_shop, CoffeeShop.Repo,
username: "postgres",
password: "postgres",
database: "coffee_shop_dev",
hostname: "localhost",
show_sensitive_data_on_connection_error: true,
pool_size: 10
I start the server and have a look at my browser at http://localhost:4000.
$ mix phx.server
I see Phoenix’s default welcome page: “Welcome to Phoenix!”. Great, I’ve got a skelton web application up, and a connection to the database. Now is a good time to commit.
Commanded
Let’s start with Commanded now. You can find the latest info on hexdocs.pm.
Add commanded to the list of deps:
{:gettext, "~> 0.11"},
{:jason, "~> 1.1"},
{:plug_cowboy, "~> 2.0"},
{:commanded, "~> 1.0"}
]
end
Install the new dependencies:
mix deps.get
I’m going to write a throw-away test here that will lead us through the installation of commanded. It’s going to start out failing and when it passes, then we’re done.
Add the following test:
# test/coffee_shop_web/controllers/throwaway_test.exs
defmodule CoffeeShopWeb.ThrowawayTest do
use CoffeeShopWeb.ConnCase
test "that shows me that commanded is working.", %{conn: conn} do
order_id = "a1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1"
params = %{
"order" => %{
"order_id" => order_id,
"for" => "Ben"
}
}
conn = post(conn, "/orders", params)
end
end
When we run it, we’ll get errors because none of this code exists yet.
$ mix test test/coffee_shop_web/controllers/throwaway_test.exs
** (Phoenix.Router.NoRouteError) no route found for POST /orders (CoffeeShopWeb.Router)
Clearly we’re missing a route:
# lib/coffee_shop_web/router.ex
...
scope "/", CoffeeShopWeb do
pipe_through :browser
get "/", PageController, :index
resources "/orders", OrderController
end
...
Next we’ll see that we don’t have an OrderController so we’ll add that.
$ mix test test/coffee_shop_web/controllers/throwaway_test.exs
** (UndefinedFunctionError) function CoffeeShopWeb.OrderController.init/1 is undefined (module CoffeeShopWeb.OrderController is not available)
# lib/coffee_shop_web/order_controller.ex
defmodule CoffeeShopWeb.OrderController do
use CoffeeShopWeb, :controller
def create(conn, params) do
IO.inspect(params)
conn
end
end
From here, the test passes and our params are passed through. It doesn’t do anything useful yet, but we’re green.
$ mix test test/coffee_shop_web/controllers/throwaway_test.exs
test/coffee_shop_web/controllers/throwaway_test.exs:14: CoffeeShopWeb.ThrowawayTest."test that shows me that commanded is working."/1
%{
"order" => %{
"for" => "Ben",
"order_id" => "a1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1"
}
}
Now we can imagine what we’d like our controller code to look like:
# lib/coffee_shop_web/controllers/order_controller.ex
def create(conn, params) do
order_params = params |> Map.fetch!("order")
case CoffeeShop.open_order(order_params) do
{:ok, order} ->
conn
|> html(order.for)
{:error, error} ->
raise error
end
end
And run our test again:
$ mix test test/coffee_shop_web/controllers/throwaway_test.exs
** (UndefinedFunctionError) function CoffeeShop.open_order/1 is undefined or private
Let’s define our open_order/1
function:
# lib/coffee_shop.ex
defmodule CoffeeShop do
alias CoffeeShop.CommandedApp
def open_order(params) do
command = %CoffeeShop.Commands.OpenOrder{
order_id: params |> Map.fetch!("order_id"),
for: params |> Map.fetch!("for")
}
case CommandedApp.dispatch(command, include_execution_result: true) do
{:ok, %{aggregate_state: order}} ->
{:ok, order}
error ->
error
end
end
end
And add the new command:
# lib/coffee_shop/commands.ex
defmodule CoffeeShop.Commands do
defmodule OpenOrder do
@enforce_keys [:order_id, :for]
defstruct @enforce_keys
end
end
Run our test:
$ mix test test/coffee_shop_web/controllers/throwaway_test.exs
** (UndefinedFunctionError) function CommandedApp.dispatch/2 is undefined (module CoffeeShop.CommandedApp is not available)
According to the commanded docs we need to “Define a Commanded application module for [our] app”. Let’s do that.
# lib/coffee_shop/commanded_app.ex
defmodule CoffeeShop.CommandedApp do
use Commanded.Application, otp_app: :coffee_shop
end
Run our test:
$ mix test test/coffee_shop_web/controllers/throwaway_test.exs
[error] attempted to dispatch an unregistered command: %CoffeeShop.Commands.OpenOrder{for: "Ben", order_id: "a1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1"}
The commanded guide says we need to choose an event store, but we can keep going. Commanded wants us to register our commands with a router so that it can dispatch them for us. We’ll register a router with our CommandedApp now.
# lib/coffee_shop.ex
defmodule CoffeeShop.CommandedApp do
use Commanded.Application, otp_app: :coffee_shop
router(CoffeeShop.CommandedRouter)
end
# lib/coffee_shop/commanded_router.ex
defmodule CoffeeShop.CommandedRouter do
use Commanded.Commands.Router
end
We get the same error when the test is run again because although we’ve created a router and hooked it up to the application, we still haven’t registered OpenOrder as a command. Reading from the docs we can implement ours:
# lib/coffee_shop/commanded_router.ex
defmodule CoffeeShop.CommandedRouter do
use Commanded.Commands.Router
alias CoffeeShop.Order
alias CoffeeShop.Commands.OpenOrder
dispatch OpenOrder, to: Order, identity: :order_id
end
Order is our aggregate, which we’re going to be dispatching our command to directly. We don’t have one, so lets create it.
# lib/coffee_shop/order.ex
defmodule CoffeeShop.Order do
end
Run our test:
$ mix test test/coffee_shop_web/controllers/throwaway_test.exs
** (ArgumentError) Command handler `CoffeeShop.Order` does not define a `execute/2` function
Since we’re dispatching to Order directly, we need to handle the command there:
# lib/coffee_shop/order.ex
defmodule CoffeeShop.Order do
defstruct [:order_id, :for, status: :initialized]
alias __MODULE__, as: Order
alias CoffeeShop.Commands.OpenOrder
alias CoffeeShop.Events.OrderOpened
def execute(%Order{order_id: nil}, %OpenOrder{} = command) do
%OrderOpened{
order_id: command.order_id,
for: command.for
}
end
def execute(%Order{order_id: id}, _) do
raise "Can't open an Order twice."
end
end
And we’ll define our event too:
# lib/coffee_shop/events.ex
defmodule CoffeeShop.Events do
defmodule OrderOpened do
@derive Jason.Encoder
defstruct [:order_id, :for, :status]
end
end
This time when we run our tests we get:
** (RuntimeError) could not lookup CoffeeShop.CommandedApp because it was not started or it does not exist
We need to add it to the list of child processes to be started when the (Elixir) app comes up.
# lib/coffee_shop/application.ex
defmodule CoffeeShop.Application do
use Application
def start(_type, _args) do
# List all child processes to be supervised
children = [
CoffeeShop.CommandedApp,
CoffeeShopWeb.Endpoint,
etc...
Our next error:
** (ArgumentError) missing :event_store config for application CoffeeShop.CommandedApp
Ah ok, now we’ve got to add the event store. Back to the Commanded docs, we’re going with Postgres so we’ll follow their getting started guid.
Postgres EventStore Adapter
Add the Postgres EventStore Adapter to the deps
# mix.exs
...
{:jason, "~> 1.1"},
{:plug_cowboy, "~> 2.0"},
{:commanded, "~> 1.0"},
{:commanded_eventstore_adapter, "~> 1.0.0"}
]
# update the deps
$ mix deps.get
We create an EventStore module as per the docs
defmodule CoffeeShop.EventStore do
use EventStore, otp_app: :coffee_shop
end
Now we need to add that missing section to the Commanded app.
# lib/coffee_shop/commanded_app.ex
defmodule CoffeeShop.CommandedApp do
use Commanded.Application,
otp_app: :coffee_shop,
event_store: [
adapter: Commanded.EventStore.Adapters.EventStore,
event_store: CoffeeShop.EventStore
]
router(CoffeeShop.CommandedRouter)
end
And configure the event store. I’m doing mine in config/config.exs, you can customize yours in config/test.exs, config/dev.exs and config/prod.exs as you see fit. As mentioned earlier, this will be a separate database than the one used for the main Repo
(coffee_shop_dev vs coffee_shop_eventstore_dev).
...
# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason
config :coffee_shop, CoffeeShop.EventStore,
serializer: Commanded.Serialization.JsonSerializer,
username: "postgres",
password: "postgres",
database: "coffee_shop_eventstore_#{Mix.env()}",
pool_size: 10
config :coffee_shop,
event_stores: [CoffeeShop.EventStore]
Now we can create the event store database
$ mix do event_store.create, event_store.init
The EventStore database has been created.
The EventStore database has been initialized.
And again for the test environment.
$ MIX_ENV=test mix do event_store.create, event_store.init
The EventStore database has been created.
The EventStore database has been initialized.
Back to the test and we’re rewarded a large, angry blood-red error. I’ll spare you the details and highlight the trouble:
** (UndefinedFunctionError) function CoffeeShop.Order.apply/2 is undefined or private
We need to apply the event our aggregate. Easy enough:
# lib/coffee_shop/order.ex
defmodule CoffeeShop.Order do
...
def apply(%Order{} = order, %OrderOpened{} = event) do
%{order | order_id: event.order_id, for: event.for, status: :opened}
end
end
Now when we run the test again, it passes!
We’re good right? Of course not. Lets run the test one more time.
[error] GenServer {CoffeeShop.CommandedApp.LocalRegistry, {CoffeeShop.CommandedApp, CoffeeShop.Order, "a1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1"}} terminating
** (RuntimeError) Can't open an Order twice.
We’re not cleaning up between test runs. Setting that up is a bit messy and I’ve cribbed it completely from Ben Smith’s sample project
You can see my commit to implement yours.
You can find the full source on github
This gets us as far as setting up the libraries and handling a command, but we haven’t covered a read-side yet. We’ll cover event handlers/projections/read side next time.
You’d also want to flesh out the pheonix side more, creating forms, validations, redirects and handling errors.