Creating a single-node Elixir/Phoenix web app with Commanded & Postgres.

Posted on

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.