Repository: commanded/commanded
Pull request #324 (2019-12-04 17:48:52)

Dynamic Commanded applications
Add support for starting multiple instances of the same Commanded application, each with a unique name. 

This feature can be used for multi-tenancy where each tenant's data is stored in a separate event store. It provides a guarantee of data segregation and isolation. It might also be used to support running unit/integration tests concurrently by starting a dynamic Commanded app and any required test processes (event handlers, PMs, etc.) per test.

### Example usage

Define a Commanded application:

```elixir
defmodule MyApp.App do
  use Commanded.Application, otp_app: :my_app
end
```

Start multiple instances of the application by providing a unique name per instance:

```elixir
for tenant <- [:tenant1, :tenant2, :tenant3] do
  {:ok, _pid} = MyApp.App.start_link(name: tenant)
end
```

Dispatch a command to a dynamic, named application:

```elixir
:ok = MyApp.dispatch(command, application: :tenant1)
```


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index df80dc8..fd4b9ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## Next release
 
+### Enhancements
+
+- Dynamic Commanded applications ([#324](https://github.com/commanded/commanded/pull/324)).
+
 ### Bug fixes
 
 - Fixes the typespec for command dispatch ([#325](https://github.com/commanded/commanded/pull/325)).
diff --git a/guides/Application.md b/guides/Application.md
index 77f73a8..faa55a3 100644
--- a/guides/Application.md
+++ b/guides/Application.md
@@ -58,7 +58,7 @@ end
 
 ## Routing commands
 
-A Commanded application is also a composite router. This provides the `router` macro allowing you to define one or more router modules for command dispatch.
+A Commanded application is also a composite router. This provides the `router` macro allowing you to include one or more router modules for command dispatch.
 
 ```elixir
 defmodule MyApp.Application do
diff --git a/guides/Supervision.md b/guides/Supervision.md
index a8981c7..d56bafa 100644
--- a/guides/Supervision.md
+++ b/guides/Supervision.md
@@ -16,12 +16,12 @@ defmodule Bank.Supervisor do
       # Application
       BankApp,
 
+      # Event handler
+      AccountBalanceHandler
+
       # Process manager
       TransferMoneyProcessManager,
 
-      # Event handler
-      AccountBalanceHandler
-      
       # Optionally, provide runtime configuration
       {WelcomeEmailHandler, start_from: :current},
     ]
diff --git a/lib/application.ex b/lib/application.ex
index f81b17d..807703c 100644
--- a/lib/application.ex
+++ b/lib/application.ex
@@ -24,8 +24,8 @@ defmodule Commanded.Application do
         pubsub: :local,
         registry: :local
 
-  Alternatively, you can include the event store, pubsub, and registry config
-  when defining the application:
+  Alternatively, you can include the configuration when defining the
+  application:
 
       defmodule MyApp.Application do
         use Commanded.Application,
@@ -40,6 +40,54 @@ defmodule Commanded.Application do
         router(MyApp.Router)
       end
 
+  A Commanded application must be started before it can be used:
+
+      {:ok, _pid} = MyApp.Application.start_link()
+
+  Instead of starting the application manually, you should use a
+  [Supervisor](supervision.html).
+
+  ## Supervision
+
+  Use a supervisor to start your Commanded application:
+
+      Supervisor.start_link([
+        MyApp.Application
+      ], strategy: :one_for_one)
+
+  ## Dynamic named applications
+
+  An application can be provided with a name as an option to `start_link/1`.
+  This can be used to start the same application multiple times, each using its
+  own separately configured and isolated event store. Each application must be
+  started with a unique name.
+
+  Multipe instances of the same event handler or process manager can be
+  started by refering to a started application by its name. The event store
+  operations can also be scoped to an application by referring to its name.
+
+  ### Example
+
+  Start an application process for each tenant in a multi-tenanted app,
+  guaranteeing that the data and processing remains isolated between tenants.
+
+      for tenant <- [:tenant1, :tenant2, :tenant3] do
+        {:ok, _app} = MyApp.Application.start_link(name: tenant)
+      end
+
+  Typically you would start the applications using a supervisor:
+
+      children =
+        for tenant <- [:tenant1, :tenant2, :tenant3] do
+          {MyApp.Application, name: tenant}
+        end
+
+      Supervisor.start_link(children, strategy: :one_for_one)
+
+  To dispatch a command you must provide the application name:
+
+      :ok = MyApp.Application.dispatch(command, application: :tenant1)
+
   """
 
   @type t :: module
@@ -53,7 +101,6 @@ defmodule Commanded.Application do
 
       @otp_app otp_app
       @config config
-      @name Keyword.get(opts, :name, __MODULE__)
 
       use Commanded.Commands.CompositeRouter, application: __MODULE__
 
@@ -66,19 +113,37 @@ defmodule Commanded.Application do
 
       def child_spec(opts) do
         %{
-          id: __MODULE__,
+          id: name(opts),
           start: {__MODULE__, :start_link, [opts]},
           type: :supervisor
         }
       end
 
       def start_link(opts \\ []) do
-        Commanded.Application.Supervisor.start_link(__MODULE__, @otp_app, @config, opts)
+        name = name(opts)
+
+        Commanded.Application.Supervisor.start_link(__MODULE__, @otp_app, @config, name, opts)
       end
 
       def stop(pid, timeout \\ 5000) do
         Supervisor.stop(pid, :normal, timeout)
       end
+
+      defp name(opts) do
+        case Keyword.get(opts, :name) do
+          nil ->
+            __MODULE__
+
+          name when is_atom(name) ->
+            name
+
+          invalid ->
+            raise ArgumentError,
+              message:
+                "expected :name option to be an atom but got: " <>
+                  inspect(invalid)
+        end
+      end
     end
   end
 
@@ -128,6 +193,24 @@ defmodule Commanded.Application do
 
   alias Commanded.Application.Config
 
+  @doc false
+  def dispatch(application, command, opts \\ [])
+
+  def dispatch(application, command, timeout) when is_integer(timeout),
+    do: dispatch(application, command, timeout: timeout)
+
+  def dispatch(application, command, :infinity),
+    do: dispatch(application, command, timeout: :infinity)
+
+  def dispatch(application, command, opts) do
+    opts = Keyword.put(opts, :application, application)
+
+    application_module(application).dispatch(command, opts)
+  end
+
+  @doc false
+  def application_module(application), do: Config.get(application, :application)
+
   @doc false
   @spec event_store_adapter(Commanded.Application.t()) :: {module, map}
   def event_store_adapter(application), do: Config.get(application, :event_store)
diff --git a/lib/commanded.ex b/lib/commanded.ex
index 710e3be..84c536f 100644
--- a/lib/commanded.ex
+++ b/lib/commanded.ex
@@ -4,6 +4,7 @@ defmodule Commanded do
 
   Provides support for:
 
+  - [Defining applications](application.html).
   - [Command registration and dispatch](commands.html).
   - [Hosting and delegation to aggregates](aggregates.html).
   - [Event handling](events.html).
@@ -11,14 +12,15 @@ defmodule Commanded do
 
   Use Commanded with one of the following event stores for persistence:
 
-  - [EventStore](https://hex.pm/packages/eventstore) Elixir library, using PostgreSQL for persistence
-  - Greg Young's [Event Store](https://eventstore.org/).
+  - Elixir [EventStore](https://hex.pm/packages/eventstore) using PostgreSQL for persistence
+  - [EventStore.org](https://eventstore.org/)
 
   Please check the [Getting Started](getting-started.html) and [Usage](usage.html) guides to learn more.
   """
 
   use Application
 
+  @doc false
   def start(_type, _args) do
     children = [
       Commanded.Application.Config
diff --git a/lib/commanded/application/supervisor.ex b/lib/commanded/application/supervisor.ex
index 863fa7b..43d4f5d 100644
--- a/lib/commanded/application/supervisor.ex
+++ b/lib/commanded/application/supervisor.ex
@@ -25,6 +25,7 @@ defmodule Commanded.Application.Supervisor do
       config
       |> Keyword.merge(opts)
       |> Keyword.put(:otp_app, otp_app)
+      |> Keyword.put(:application, application)
 
     case application_init(application, config) do
       {:ok, config} -> {:ok, config}
@@ -35,30 +36,28 @@ defmodule Commanded.Application.Supervisor do
   @doc """
   Starts the application supervisor.
   """
-  def start_link(application, otp_app, config, opts) do
-    sup_opts = if name = Keyword.get(opts, :name, application), do: [name: name], else: []
-
+  def start_link(application, otp_app, config, name, opts) do
     Supervisor.start_link(
       __MODULE__,
-      {application, otp_app, config, opts},
-      sup_opts
+      {application, otp_app, config, name, opts},
+      name: name
     )
   end
 
-  def init({application, otp_app, config, opts}) do
+  def init({application, otp_app, config, name, opts}) do
     case runtime_config(application, otp_app, config, opts) do
       {:ok, config} ->
-        {event_store_child_spec, config} = event_store_child_spec(application, config)
-        {pubsub_child_spec, config} = pubsub_child_spec(application, config)
-        {registry_child_spec, config} = registry_child_spec(application, config)
+        {event_store_child_spec, config} = event_store_child_spec(name, config)
+        {pubsub_child_spec, config} = pubsub_child_spec(name, config)
+        {registry_child_spec, config} = registry_child_spec(name, config)
 
-        :ok = Config.associate(self(), application, config)
+        :ok = Config.associate(self(), name, config)
 
         children =
           event_store_child_spec ++
             pubsub_child_spec ++
             registry_child_spec ++
-            commanded_child_spec(application, config)
+            app_child_spec(name, config)
 
         Supervisor.init(children, strategy: :one_for_one)
 
@@ -67,47 +66,47 @@ defmodule Commanded.Application.Supervisor do
     end
   end
 
-  defp commanded_child_spec(application, config) do
-    task_dispatcher_name = Module.concat([application, Commanded.Commands.TaskDispatcher])
-    subscriptions_name = Module.concat([application, Commanded.Subscriptions])
-    registry_name = Module.concat([application, Commanded.Subscriptions.Registry])
+  defp app_child_spec(name, config) do
+    task_dispatcher_name = Module.concat([name, Commanded.Commands.TaskDispatcher])
+    subscriptions_name = Module.concat([name, Commanded.Subscriptions])
+    registry_name = Module.concat([name, Commanded.Subscriptions.Registry])
     snapshotting = Keyword.get(config, :snapshotting, %{})
 
     [
       {Task.Supervisor, name: task_dispatcher_name},
-      {Commanded.Aggregates.Supervisor, application: application, snapshotting: snapshotting},
-      {Commanded.Subscriptions.Registry, application: application, name: registry_name},
-      {Commanded.Subscriptions, application: application, name: subscriptions_name}
+      {Commanded.Aggregates.Supervisor, application: name, snapshotting: snapshotting},
+      {Commanded.Subscriptions.Registry, application: name, name: registry_name},
+      {Commanded.Subscriptions, application: name, name: subscriptions_name}
     ]
   end
 
-  defp event_store_child_spec(application, config) do
+  defp event_store_child_spec(name, config) do
     {adapter, adapter_config} =
-      Commanded.EventStore.adapter(application, Keyword.get(config, :event_store))
+      Commanded.EventStore.adapter(name, Keyword.get(config, :event_store))
 
-    {:ok, child_spec, adapter_meta} = adapter.child_spec(application, adapter_config)
+    {:ok, child_spec, adapter_meta} = adapter.child_spec(name, adapter_config)
 
     config = Keyword.put(config, :event_store, {adapter, adapter_meta})
 
     {child_spec, config}
   end
 
-  defp pubsub_child_spec(application, config) do
+  defp pubsub_child_spec(name, config) do
     {adapter, adapter_config} =
-      Commanded.PubSub.adapter(application, Keyword.get(config, :pubsub, :local))
+      Commanded.PubSub.adapter(name, Keyword.get(config, :pubsub, :local))
 
-    {:ok, child_spec, adapter_meta} = adapter.child_spec(application, adapter_config)
+    {:ok, child_spec, adapter_meta} = adapter.child_spec(name, adapter_config)
 
     config = Keyword.put(config, :pubsub, {adapter, adapter_meta})
 
     {child_spec, config}
   end
 
-  defp registry_child_spec(application, config) do
+  defp registry_child_spec(name, config) do
     {adapter, adapter_config} =
-      Commanded.Registration.adapter(application, Keyword.get(config, :registry, :local))
+      Commanded.Registration.adapter(name, Keyword.get(config, :registry, :local))
 
-    {:ok, child_spec, adapter_meta} = adapter.child_spec(application, adapter_config)
+    {:ok, child_spec, adapter_meta} = adapter.child_spec(name, adapter_config)
 
     config = Keyword.put(config, :registry, {adapter, adapter_meta})
 
diff --git a/lib/commanded/event/handler.ex b/lib/commanded/event/handler.ex
index 457d9c0..f3d49d0 100644
--- a/lib/commanded/event/handler.ex
+++ b/lib/commanded/event/handler.ex
@@ -52,6 +52,12 @@ defmodule Commanded.Event.Handler do
   to send transactional emails to an already deployed system containing many
   historical events.
 
+  The `start_from` option only applies when the subscription is initially
+  created, the first time the handler starts. Whenever the handler restarts the
+  subscription will resume from the next event after the last successfully
+  processed event. Restarting an event handler does not restart its
+  subscription.
+
   ### Example
 
   Set the `start_from` option (`:origin`, `:current`, or an explicit event
@@ -69,7 +75,6 @@ defmodule Commanded.Event.Handler do
 
       {:ok, _handler} = ExampleHandler.start_link(start_from: :current)
 
-
   ### Subscribing to an individual stream
 
   By default event handlers will subscribe to all events appended to any stream.
@@ -184,6 +189,8 @@ defmodule Commanded.Event.Handler do
 
   ### Example
 
+  Define an event handler with `:strong` consistency:
+
       defmodule ExampleHandler do
         use Commanded.Event.Handler,
           application: ExampleApp,
@@ -191,6 +198,33 @@ defmodule Commanded.Event.Handler do
           consistency: :strong
       end
 
+  ## Dynamic application
+
+  An event handler's application can be provided as an option to `start_link/1`.
+  This can be used to start the same handler multiple times, each using a
+  separate Commanded application and event store.
+
+  ### Example
+
+  Start an event handler process for each tenant in a multi-tenanted app,
+  guaranteeing that the data and processing remains isolated between tenants.
+
+      for tenant <- [:tenant1, :tenant2, :tenant3] do
+        {:ok, _app} = MyApp.Application.start_link(name: tenant)
+        {:ok, _handler} = ExampleHandler.start_link(application: tenant)
+      end
+
+  Typically you would start the event handlers using a supervisor:
+
+      children =
+        for tenant <- [:tenant1, :tenant2, :tenant3] do
+          {ExampleHandler, application: tenant}
+        end
+
+      Supervisor.start_link(children, strategy: :one_for_one)
+
+  The above example requires three named Commanded applications to have already
+  been started.
   """
 
   use GenServer
@@ -282,10 +316,11 @@ defmodule Commanded.Event.Handler do
 
       @doc false
       def start_link(opts \\ []) do
+        application = Keyword.get(opts, :application, @application)
         module_opts = Keyword.drop(@opts, [:application, :name])
         opts = Handler.start_opts(__MODULE__, module_opts, opts)
 
-        Handler.start_link(@application, @name, __MODULE__, opts)
+        Handler.start_link(application, @name, __MODULE__, opts)
       end
 
       @doc """
@@ -300,8 +335,10 @@ defmodule Commanded.Event.Handler do
 
       """
       def child_spec(opts) do
+        application = Keyword.get(opts, :application, @application)
+
         default = %{
-          id: {__MODULE__, @name},
+          id: {__MODULE__, application, @name},
           start: {__MODULE__, :start_link, [opts]},
           restart: :permanent,
           type: :worker
@@ -349,7 +386,9 @@ defmodule Commanded.Event.Handler do
     {valid, invalid} =
       module_opts
       |> Keyword.merge(local_opts)
-      |> Keyword.split([:consistency, :start_from, :subscribe_to] ++ additional_allowed_opts)
+      |> Keyword.split(
+        [:application, :consistency, :start_from, :subscribe_to] ++ additional_allowed_opts
+      )
 
     if Enum.any?(invalid) do
       raise ArgumentError,
diff --git a/lib/commanded/process_managers/process_manager.ex b/lib/commanded/process_managers/process_manager.ex
index ff92d77..6e3b11f 100644
--- a/lib/commanded/process_managers/process_manager.ex
+++ b/lib/commanded/process_managers/process_manager.ex
@@ -6,6 +6,10 @@ defmodule Commanded.ProcessManagers.ProcessManager do
   It handles events and dispatches commands in response. Process managers have
   state that can be used to track which aggregates are being orchestrated.
 
+  Process managers can be used to implement long-running transactions by
+  following the saga pattern. This is a sequence of commands and their
+  compensating commands which can be used to rollback on failure.
+
   Use the `Commanded.ProcessManagers.ProcessManager` macro in your process
   manager module and implement the callback functions defined in the behaviour:
 
@@ -14,6 +18,9 @@ defmodule Commanded.ProcessManagers.ProcessManager do
   - `c:apply/2`
   - `c:error/3`
 
+  Please read the [Process managers](process-managers.html) guide for more
+  detail.
+
   ### Example
 
       defmodule ExampleProcessManager do
@@ -146,6 +153,8 @@ defmodule Commanded.ProcessManagers.ProcessManager do
 
   ### Example
 
+  Define a process manager with `:strong` consistency:
+
       defmodule ExampleProcessManager do
         use Commanded.ProcessManagers.ProcessManager,
           application: ExampleApp,
@@ -153,8 +162,33 @@ defmodule Commanded.ProcessManagers.ProcessManager do
           consistency: :strong
       end
 
-  Please read the [Process managers](process-managers.html) guide for more
-  detail.
+  ## Dynamic application
+
+  A process manager's application can be provided as an option to `start_link/1`.
+  This can be used to start the same process manager multiple times, each using a
+  separate Commanded application and event store.
+
+  ### Example
+
+  Start an process manager for each tenant in a multi-tenanted app, guaranteeing
+  that the data and processing remains isolated between tenants.
+
+      for tenant <- [:tenant1, :tenant2, :tenant3] do
+        {:ok, _app} = MyApp.Application.start_link(name: tenant)
+        {:ok, _handler} = ExampleProcessManager.start_link(application: tenant)
+      end
+
+  Typically you would start the event handlers using a supervisor:
+
+      children =
+        for tenant <- [:tenant1, :tenant2, :tenant3] do
+          {ExampleProcessManager, application: tenant}
+        end
+
+      Supervisor.start_link(children, strategy: :one_for_one)
+
+  The above example requires three named Commanded applications to have already
+  been started.
   """
 
   alias Commanded.ProcessManagers.FailureContext
@@ -301,10 +335,11 @@ defmodule Commanded.ProcessManagers.ProcessManager do
       @name name
 
       def start_link(opts \\ []) do
+        application = Keyword.get(opts, :application, @application)
         module_opts = Keyword.drop(@opts, [:application, :name])
         opts = Handler.start_opts(__MODULE__, module_opts, opts, [:event_timeout, :idle_timeout])
 
-        ProcessRouter.start_link(@application, @name, __MODULE__, opts)
+        ProcessRouter.start_link(application, @name, __MODULE__, opts)
       end
 
       @doc """
@@ -319,8 +354,10 @@ defmodule Commanded.ProcessManagers.ProcessManager do
 
       """
       def child_spec(opts) do
+        application = Keyword.get(opts, :application, @application)
+
         default = %{
-          id: {__MODULE__, @name},
+          id: {__MODULE__, application, @name},
           start: {__MODULE__, :start_link, [opts]},
           restart: :permanent,
           type: :worker
diff --git a/lib/commanded/process_managers/process_manager_instance.ex b/lib/commanded/process_managers/process_manager_instance.ex
index 08f8521..4a76591 100644
--- a/lib/commanded/process_managers/process_manager_instance.ex
+++ b/lib/commanded/process_managers/process_manager_instance.ex
@@ -5,6 +5,7 @@ defmodule Commanded.ProcessManagers.ProcessManagerInstance do
 
   require Logger
 
+  alias Commanded.Application
   alias Commanded.ProcessManagers.{ProcessRouter, FailureContext}
   alias Commanded.EventStore
   alias Commanded.EventStore.{RecordedEvent, SnapshotData}
@@ -162,11 +163,11 @@ defmodule Commanded.ProcessManagers.ProcessManagerInstance do
     {:noreply, state, idle_timeout}
   end
 
-  defp process_unseen_event(event, %State{} = state, context \\ %{}) do
+  defp process_unseen_event(%RecordedEvent{} = event, %State{} = state, context \\ %{}) do
     %RecordedEvent{correlation_id: correlation_id, event_id: event_id, event_number: event_number} =
       event
 
-    %State{application: application, idle_timeout: idle_timeout} = state
+    %State{idle_timeout: idle_timeout} = state
 
     case handle_event(event, state) do
       {:error, error} ->
@@ -182,7 +183,7 @@ defmodule Commanded.ProcessManagers.ProcessManagerInstance do
 
       commands ->
         # Copy event id, as causation id, and correlation id from handled event.
-        opts = [application: application, causation_id: event_id, correlation_id: correlation_id]
+        opts = [causation_id: event_id, correlation_id: correlation_id]
 
         with :ok <- commands |> List.wrap() |> dispatch_commands(opts, state, event) do
           process_state = mutate_state(event, state)
@@ -221,13 +222,9 @@ defmodule Commanded.ProcessManagers.ProcessManagerInstance do
     end
   end
 
-  defp handle_event_error(error, failed_event, state, context) do
+  defp handle_event_error(error, %RecordedEvent{} = failed_event, %State{} = state, context) do
     %RecordedEvent{data: data} = failed_event
-
-    %State{
-      idle_timeout: idle_timeout,
-      process_manager_module: process_manager_module
-    } = state
+    %State{idle_timeout: idle_timeout, process_manager_module: process_manager_module} = state
 
     failure_context = %FailureContext{
       pending_commands: [],
@@ -299,12 +296,12 @@ defmodule Commanded.ProcessManagers.ProcessManagerInstance do
       describe(state) <> " attempting to dispatch command: #{inspect(command)}"
     end)
 
-    case application.dispatch(command, opts) do
+    case Application.dispatch(application, command, opts) do
       :ok ->
         dispatch_commands(pending_commands, opts, state, last_event)
 
-      # when include_execution_result is set to true, the dispatcher returns an :ok tuple
-      {:ok, _} ->
+      # When `include_execution_result` is set to true (globally), the dispatcher returns an :ok tuple
+      {:ok, _result} ->
         dispatch_commands(pending_commands, opts, state, last_event)
 
       error ->


Test patch:
diff --git a/test/application/application_test.exs b/test/application/application_test.exs
index c1ac7bf..2199b10 100644
--- a/test/application/application_test.exs
+++ b/test/application/application_test.exs
@@ -13,6 +13,10 @@ defmodule Commanded.ApplicationTest do
       [pid: pid]
     end
 
+    test "should register process using application name", %{pid: pid} do
+      assert Process.whereis(ExampleApplication) == pid
+    end
+
     test "should not allow an application to be started more than once", %{pid: pid} do
       assert {:error, {:already_started, ^pid}} = ExampleApplication.start_link()
     end
diff --git a/test/application/dynamic_applications_test.exs b/test/application/dynamic_applications_test.exs
new file mode 100644
index 0000000..586f510
--- /dev/null
+++ b/test/application/dynamic_applications_test.exs
@@ -0,0 +1,111 @@
+defmodule Commanded.DynamicApplicationsTest do
+  use ExUnit.Case
+
+  alias Commanded.ExampleApplication
+  alias Commanded.ExampleDomain.BankAccount.Commands.OpenAccount
+  alias Commanded.Helpers.CommandAuditMiddleware
+  alias Commanded.ReplyHandler
+
+  setup_all do
+    start_supervised!(CommandAuditMiddleware)
+    :ok
+  end
+
+  describe "dynamic Commanded applications" do
+    test "should ensure name is an atom" do
+      assert_raise ArgumentError,
+                   "expected :name option to be an atom but got: \"invalid\"",
+                   fn -> ExampleApplication.start_link(name: "invalid") end
+    end
+
+    test "should allow name to be provided when starting an application" do
+      assert {:ok, pid} = start_supervised({ExampleApplication, name: :example1})
+      assert Process.whereis(:example1) == pid
+    end
+
+    test "should not allow an application to be started more than once with the same name" do
+      assert {:ok, pid} = start_supervised({ExampleApplication, name: :example1})
+      assert {:error, {:already_started, ^pid}} = ExampleApplication.start_link(name: :example1)
+    end
+
+    test "should allow the same application to be started multiple times with dynamic names" do
+      assert {:ok, pid1} = start_supervised({ExampleApplication, name: :example1})
+      assert {:ok, pid2} = start_supervised({ExampleApplication, name: :example2})
+      assert {:ok, pid3} = start_supervised({ExampleApplication, name: :example3})
+
+      assert length(Enum.uniq([pid1, pid2, pid3])) == 3
+    end
+  end
+
+  describe "dynamic application command dispatch" do
+    setup do
+      start_supervised!({ExampleApplication, name: :example1})
+      start_supervised!({ExampleApplication, name: :example2})
+      :ok
+    end
+
+    test "should dispatch to named application" do
+      {:ok, account_number} = open_account(:example1)
+
+      assert Commanded.EventStore.stream_forward(:example1, account_number) |> length() == 1
+
+      assert {:error, :stream_not_found} =
+               Commanded.EventStore.stream_forward(:example2, account_number)
+
+      {:ok, ^account_number} = open_account(:example2, account_number)
+
+      assert Commanded.EventStore.stream_forward(:example1, account_number) |> length() == 1
+      assert Commanded.EventStore.stream_forward(:example2, account_number) |> length() == 1
+    end
+
+    test "should error without application name" do
+      account_number = UUID.uuid4()
+      command = %OpenAccount{account_number: account_number, initial_balance: 1_000}
+
+      assert_raise RuntimeError, fn ->
+        :ok = ExampleApplication.dispatch(command)
+      end
+    end
+  end
+
+  describe "dynamic application event handler" do
+    setup do
+      start_reply_to_agent!()
+      start_supervised!({ExampleApplication, name: :example1})
+      start_supervised!({ExampleApplication, name: :example2})
+      :ok
+    end
+
+    test "should only receive events from named application" do
+      pid1 = start_supervised!({ReplyHandler, application: :example1})
+      pid2 = start_supervised!({ReplyHandler, application: :example2})
+
+      {:ok, _account_number} = open_account(:example1)
+
+      assert_receive {:event, ^pid1, _event}
+      refute_receive {:event, ^pid2, _event}
+
+      {:ok, _account_number} = open_account(:example2)
+
+      assert_receive {:event, ^pid2, _event}
+      refute_receive {:event, ^pid1, _event}
+    end
+  end
+
+  defp start_reply_to_agent! do
+    reply_to = self()
+
+    start_supervised!(%{
+      id: Agent,
+      start: {Agent, :start_link, [fn -> reply_to end, [name: :reply_to]]}
+    })
+  end
+
+  defp open_account(application, account_number \\ UUID.uuid4()) do
+    command = %OpenAccount{account_number: account_number, initial_balance: 1_000}
+
+    :ok = ExampleApplication.dispatch(command, application: application)
+
+    {:ok, account_number}
+  end
+end
diff --git a/test/application/support/reply_handler.ex b/test/application/support/reply_handler.ex
new file mode 100644
index 0000000..b15c5a7
--- /dev/null
+++ b/test/application/support/reply_handler.ex
@@ -0,0 +1,13 @@
+defmodule Commanded.ReplyHandler do
+  use Commanded.Event.Handler,
+    application: Commanded.ExampleApplication,
+    name: __MODULE__
+
+  def handle(event, _metadata) do
+    reply_to = Agent.get(:reply_to, fn reply_to -> reply_to end)
+
+    send(reply_to, {:event, self(), event})
+
+    :ok
+  end
+end
diff --git a/test/process_managers/dynamic_process_manager_application_test.exs b/test/process_managers/dynamic_process_manager_application_test.exs
new file mode 100644
index 0000000..abe97a9
--- /dev/null
+++ b/test/process_managers/dynamic_process_manager_application_test.exs
@@ -0,0 +1,77 @@
+defmodule Commanded.ProcessManager.DynamicProcessManagerApplicationTest do
+  use ExUnit.Case
+
+  alias Commanded.ExampleDomain.BankApp
+  alias Commanded.ExampleDomain.BankAccount.Commands.OpenAccount
+  alias Commanded.ExampleDomain.BankAccount.Events.BankAccountOpened
+  alias Commanded.ExampleDomain.BankAccount.Events.MoneyDeposited
+  alias Commanded.Helpers.CommandAuditMiddleware
+  alias Commanded.Helpers.Wait
+  alias Commanded.ProcessManagers.ProcessRouter
+  alias Commanded.ProcessManagers.DynamicProcessManager
+
+  setup_all do
+    start_supervised!(CommandAuditMiddleware)
+    :ok
+  end
+
+  describe "dynamic application process manager" do
+    setup do
+      start_reply_to_agent!()
+      start_supervised!({BankApp, name: :example1})
+      start_supervised!({BankApp, name: :example2})
+      :ok
+    end
+
+    test "should only receive events from named application" do
+      pid1 = start_supervised!({DynamicProcessManager, application: :example1})
+      pid2 = start_supervised!({DynamicProcessManager, application: :example2})
+
+      {:ok, account_number} = open_account(:example1)
+
+      instance1 = wait_for_process_instance(pid1, account_number)
+
+      assert_receive {:event, ^instance1, %BankAccountOpened{account_number: ^account_number}}
+      assert_receive {:event, ^instance1, %MoneyDeposited{account_number: ^account_number}}
+      refute_receive {:event, _pid, _event}
+
+      assert ProcessRouter.process_instance(pid2, account_number) ==
+               {:error, :process_manager_not_found}
+
+      {:ok, account_number} = open_account(:example2)
+
+      instance2 = wait_for_process_instance(pid2, account_number)
+
+      assert_receive {:event, ^instance2, %BankAccountOpened{account_number: ^account_number}}
+      assert_receive {:event, ^instance2, %MoneyDeposited{account_number: ^account_number}}
+      refute_receive {:event, ^instance1, _event}
+    end
+  end
+
+  defp start_reply_to_agent! do
+    reply_to = self()
+
+    start_supervised!(%{
+      id: Agent,
+      start: {Agent, :start_link, [fn -> reply_to end, [name: :reply_to]]}
+    })
+  end
+
+  defp open_account(application, account_number \\ UUID.uuid4()) do
+    command = %OpenAccount{account_number: account_number, initial_balance: 1_000}
+
+    :ok = BankApp.dispatch(command, application: application)
+
+    {:ok, account_number}
+  end
+
+  defp wait_for_process_instance(process_router, aggregate_uuid) do
+    Wait.until(fn ->
+      process_instance = ProcessRouter.process_instance(process_router, aggregate_uuid)
+
+      assert is_pid(process_instance)
+
+      process_instance
+    end)
+  end
+end
diff --git a/test/process_managers/support/dynamic_process_manager.ex b/test/process_managers/support/dynamic_process_manager.ex
new file mode 100644
index 0000000..f091ac1
--- /dev/null
+++ b/test/process_managers/support/dynamic_process_manager.ex
@@ -0,0 +1,51 @@
+defmodule Commanded.ProcessManagers.DynamicProcessManager do
+  @moduledoc false
+
+  alias Commanded.ProcessManagers.DynamicProcessManager
+  alias Commanded.ExampleDomain.BankAccount.Commands.DepositMoney
+  alias Commanded.ExampleDomain.BankAccount.Events.BankAccountOpened
+  alias Commanded.ExampleDomain.BankAccount.Events.MoneyDeposited
+  alias Commanded.ExampleDomain.BankApp
+
+  use Commanded.ProcessManagers.ProcessManager,
+    application: BankApp,
+    name: __MODULE__
+
+  @derive Jason.Encoder
+  defstruct [:account_number]
+
+  def interested?(%BankAccountOpened{account_number: account_number}),
+    do: {:start, account_number}
+
+  def interested?(%MoneyDeposited{account_number: account_number}),
+    do: {:continue!, account_number}
+
+  def handle(%DynamicProcessManager{}, %BankAccountOpened{} = event) do
+    %BankAccountOpened{account_number: account_number} = event
+
+    send_reply(event)
+
+    # Deposit account opening welcome bonus
+    [
+      %DepositMoney{account_number: account_number, transfer_uuid: UUID.uuid4(), amount: 100}
+    ]
+  end
+
+  def handle(%DynamicProcessManager{}, %MoneyDeposited{} = event) do
+    send_reply(event)
+
+    []
+  end
+
+  def apply(%DynamicProcessManager{} = pm, %BankAccountOpened{} = event) do
+    %BankAccountOpened{account_number: account_number} = event
+
+    %DynamicProcessManager{pm | account_number: account_number}
+  end
+
+  defp send_reply(event) do
+    reply_to = Agent.get(:reply_to, fn reply_to -> reply_to end)
+
+    send(reply_to, {:event, self(), event})
+  end
+end