Skip to content

Instantly share code, notes, and snippets.

@slashdotdash
Last active July 21, 2017 12:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save slashdotdash/2e145d28868f0ed55d9fd6ef693a8132 to your computer and use it in GitHub Desktop.
Save slashdotdash/2e145d28868f0ed55d9fd6ef693a8132 to your computer and use it in GitHub Desktop.
Domain-driven design shipping example implemented using Commanded

Domain-driven design shipping example implemented using Commanded

Inspired by Peter C Marks' ddd_elixir_stage1_umbrella repo.

I've implemented the cargo event handling workflow using the approach dictated by the Commanded CQRS/ES library for Elixir.

The flow is as follows:

  1. HandlingEventController web controller constructs a TrackHandling command struct from given POST params.
  2. TrackHandling command is dispatched using the CommandRouter, which has configured the command to handler/aggregate mapping.
  3. TrackHandling command is routed to the CargoTracking module and a GenServer is started/resumed to host the Cargo aggregate.
  4. The CargoTracking module returns a HandlingEvent domain event, which is appended to the events for the Cargo aggregate identified by cargo_id from the command.
  5. The Cargo.apply/2 function is called to mutate the cargo state, appending the handling event to its list of handling events. This function is also used when rebuilding the cargo state by replaying its events.

TrackHandling => CommandRouter => CargoTracking => HandlingEvent => Cargo

Commanded provides the means to host each aggregate (e.g. Cargo) within its own Elixir process. It is responsible for rehydrating the aggregate from its persisted events, and appending new events to storage. You can configure event handlers in Commanded, such as to project events into a denormalised read model. The read model is queried for data to be returned to the end user.

Take a look at Conduit if you would like to see an entire Elixir application built using Commanded following the CQRS/ES pattern.

defmodule Cargo do
@doc """
Cargo aggregate
"""
defstruct [
id: nil,
status: nil,
handling_events: [],
]
# record handling event for this cargo
def apply(%Cargo{} = cargo, %HandlingEvent{status: status} = handling) do
%Cargo{cargo |
handling_events: [handling | handling_events],
status: status,
}
end
end
defmodule CargoTracking do
@doc """
Cargo tracking command handler/workflow
"""
def handle(%Cargo{cargo_id: cargo_id, status: status}, %TrackHandling{cargo_id: cargo_id, type: type, completion_time: completion_time, registration_time: registration_time}) do
%HandlingEvent{
cargo_id: cargo_id,
completion_time: completion_time,
registration_time: registration_time,
status: next_status(type, status),
}
end
defp next_status("RECEIVE", "BOOKED"), do: {:on_track, "IN PORT"}
defp next_status("CUSTOMS", "IN PORT"), do: {:on_track, "IN PORT"}
defp next_status("CLAIM", "IN PORT"), do: {:on_track, "CLAIMED"}
defp next_status("LOAD", "CLEARED"), do: {:on_track, "ON CARRIER"}
defp next_status("LOAD", "RECEIVED"), do: {:on_track, "ON CARRIER"}
defp next_status("LOAD", "IN PORT"), do: {:on_track, "ON CARRIER"}
defp next_status("LOAD", "ON CARRIER"), do: {:off_track, "ON CARRIER"}
defp next_status("UNLOAD", "ON CARRIER"), do: {:on_track, "IN PORT"}
defp next_status("UNLOAD", "IN PORT"), do: {:off_track, "IN PORT"}
defp next_status(_, status), do: {:off_track, status}
end
defmodule CommandRouter do
@doc """
Command registration and dispatcher
"""
use Commanded.Commands.Router
dispatch [TrackHandling], to: CargoTracking, aggregate: Cargo, identity: :cargo_id
end
defmodule HandlingEvent do
@doc """
Handling event using Poison for JSON enconding
"""
@derive [Poison.Encoder]
defstruct [
:cargo_id,
:completion_time,
:registration_time,
:status,
]
end
defmodule Shipping.Web.Tracking.HandlingEventController do
use Shipping.Web, :controller
def create(conn, %{"handling_event" => params}) do
case params |> TrackHandling.new() |> CommandRouter.dispatch() do
:ok ->
conn
|> put_flash(:info, "Handling event created successfully.")
|> redirect(to: tracking_handling_event_path(conn, :index))
{:error, reason} ->
render(conn, "new.html", params: params, failure: reason)
end
end
end
defmodule TrackHandling do
@doc """
Command to track a single cargo handling
"""
defstruct [
:cargo_id,
:completion_time,
:registration_time,
]
use ExConstructor
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment