Repository: commanded/commanded
Pull request #331 (2019-12-19 12:12:17)

Command dispatch return
By default a successful command dispatch will return `:ok`. This pull request allows changing the default behaviour by specifying a `returning` option during command dispatch.

The supported options are:

- `:aggregate_state` - to return the update aggregate state.

- `:aggregate_version` - to return only the aggregate version.

- `:execution_result` - to return a `Commanded.Commands.ExecutionResult`
  struct containing the aggregate's identity, state, version, and any events
  produced from the command along with their associated metadata.

- `false` - don't return anything except an `:ok` (default).

Note this only affects successful command dispatch. If the command dispatch produces an error then it will always be returned, irrespective of the `returning` option.

### Example

```elixir
{:ok, %BankAccount{}} = BankApp.dispatch(command, returning: :aggregate_state)
```


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd4b9ca..26c5533 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 ### Enhancements
 
 - Dynamic Commanded applications ([#324](https://github.com/commanded/commanded/pull/324)).
+- Command dispatch return ([#331](https://github.com/commanded/commanded/pull/331)).
 
 ### Bug fixes
 
diff --git a/lib/application.ex b/lib/application.ex
index 807703c..3c12daf 100644
--- a/lib/application.ex
+++ b/lib/application.ex
@@ -55,6 +55,23 @@ defmodule Commanded.Application do
         MyApp.Application
       ], strategy: :one_for_one)
 
+  ## Command routing
+
+  Commanded applications are also composite routers allowing you to include
+  one or more routers within an application.
+
+  ### Example
+
+      defmodule MyApp.Application do
+        use Commanded.Application, otp_app: :my_app
+
+        router(MyApp.Accounts.Router)
+        router(MyApp.Billing.Router)
+        router(MyApp.Notifications.Router)
+      end
+
+  See `Commanded.Commands.CompositeRouter` for details.
+
   ## Dynamic named applications
 
   An application can be provided with a name as an option to `start_link/1`.
@@ -88,6 +105,24 @@ defmodule Commanded.Application do
 
       :ok = MyApp.Application.dispatch(command, application: :tenant1)
 
+
+  ## Default dispatch options
+
+  An application can be configured with default command dispatch options such as
+  `:consistency`, `:timeout`, and `:returning`. Any defaults will be used
+  unless overridden by options provided to the dispatch function.
+
+      defmodule MyApp.Application do
+        use Commanded.Application,
+          otp_app: :my_app,
+          default_dispatch_opts: [
+            consistency: :eventual,
+            returning: :aggregate_version
+          ]
+      end
+
+  See the `Commanded.Commands.Router` module for more details about the
+  supported options.
   """
 
   @type t :: module
@@ -102,7 +137,9 @@ defmodule Commanded.Application do
       @otp_app otp_app
       @config config
 
-      use Commanded.Commands.CompositeRouter, application: __MODULE__
+      use Commanded.Commands.CompositeRouter,
+        application: __MODULE__,
+        default_dispatch_opts: Keyword.get(opts, :default_dispatch_opts, [])
 
       def config do
         {:ok, config} =
@@ -185,8 +222,9 @@ defmodule Commanded.Application do
   """
   @callback dispatch(command :: struct, timeout_or_opts :: integer | :infinity | Keyword.t()) ::
               :ok
-              | {:ok, execution_result :: Commanded.Commands.ExecutionResult.t()}
+              | {:ok, aggregate_state :: struct}
               | {:ok, aggregate_version :: non_neg_integer()}
+              | {:ok, execution_result :: Commanded.Commands.ExecutionResult.t()}
               | {:error, :unregistered_command}
               | {:error, :consistency_timeout}
               | {:error, reason :: term}
diff --git a/lib/commanded/aggregates/aggregate.ex b/lib/commanded/aggregates/aggregate.ex
index 9b86e84..3726ad0 100644
--- a/lib/commanded/aggregates/aggregate.ex
+++ b/lib/commanded/aggregates/aggregate.ex
@@ -47,11 +47,13 @@ defmodule Commanded.Aggregates.Aggregate do
 
   require Logger
 
-  alias Commanded.Aggregates.{Aggregate, ExecutionContext}
+  alias Commanded.Aggregates.Aggregate
+  alias Commanded.Aggregates.ExecutionContext
   alias Commanded.Event.Mapper
   alias Commanded.Event.Upcast
   alias Commanded.EventStore
-  alias Commanded.EventStore.{RecordedEvent, SnapshotData}
+  alias Commanded.EventStore.RecordedEvent
+  alias Commanded.EventStore.SnapshotData
   alias Commanded.Registration
   alias Commanded.Snapshotting
 
@@ -227,16 +229,18 @@ defmodule Commanded.Aggregates.Aggregate do
 
     lifespan_timeout =
       case reply do
-        {:ok, _stream_version, []} ->
+        {:ok, []} ->
           aggregate_lifespan_timeout(lifespan, :after_command, command)
 
-        {:ok, _stream_version, events} ->
+        {:ok, events} ->
           aggregate_lifespan_timeout(lifespan, :after_event, events)
 
         {:error, error} ->
           aggregate_lifespan_timeout(lifespan, :after_error, error)
       end
 
+    reply = ExecutionContext.format_reply(reply, context, state)
+
     state = %Aggregate{state | lifespan_timeout: lifespan_timeout}
 
     %Aggregate{aggregate_version: aggregate_version, snapshotting: snapshotting} = state
@@ -443,60 +447,35 @@ defmodule Commanded.Aggregates.Aggregate do
 
   defp execute_command(%ExecutionContext{} = context, %Aggregate{} = state) do
     %ExecutionContext{command: command, handler: handler, function: function} = context
-    %Aggregate{aggregate_version: expected_version, aggregate_state: aggregate_state} = state
+    %Aggregate{aggregate_state: aggregate_state} = state
 
     Logger.debug(fn -> describe(state) <> " executing command: #{inspect(command)}" end)
 
-    {reply, state} =
-      case Kernel.apply(handler, function, [aggregate_state, command]) do
-        {:error, _error} = reply ->
-          {reply, state}
-
-        none when none in [:ok, nil, []] ->
-          {{:ok, expected_version, []}, state}
-
-        %Commanded.Aggregate.Multi{} = multi ->
-          case Commanded.Aggregate.Multi.run(multi) do
-            {:error, _error} = reply ->
-              {reply, state}
-
-            {aggregate_state, pending_events} ->
-              persist_events(pending_events, aggregate_state, context, state)
-          end
-
-        {:ok, pending_events} ->
-          record_events(pending_events, context, state)
-
-        pending_events ->
-          record_events(pending_events, context, state)
-      end
-
-    case reply do
-      {:error, :wrong_expected_version} ->
-        # Fetch missing events from event store
-        state = rebuild_from_events(state)
-
-        # Retry command if there are any attempts left
-        case ExecutionContext.retry(context) do
-          {:ok, context} ->
-            Logger.debug(fn -> describe(state) <> " wrong expected version, retrying command" end)
+    case Kernel.apply(handler, function, [aggregate_state, command]) do
+      {:error, _error} = reply ->
+        {reply, state}
 
-            execute_command(context, state)
-
-          reply ->
-            Logger.debug(fn ->
-              describe(state) <> " wrong expected version, but not retrying command"
-            end)
+      none when none in [:ok, nil, []] ->
+        {{:ok, []}, state}
 
+      %Commanded.Aggregate.Multi{} = multi ->
+        case Commanded.Aggregate.Multi.run(multi) do
+          {:error, _error} = reply ->
             {reply, state}
+
+          {aggregate_state, pending_events} ->
+            persist_events(pending_events, aggregate_state, context, state)
         end
 
-      reply ->
-        {reply, state}
+      {:ok, pending_events} ->
+        apply_and_persist_events(pending_events, context, state)
+
+      pending_events ->
+        apply_and_persist_events(pending_events, context, state)
     end
   end
 
-  defp record_events(pending_events, context, %Aggregate{} = state) do
+  defp apply_and_persist_events(pending_events, context, %Aggregate{} = state) do
     %Aggregate{aggregate_module: aggregate_module, aggregate_state: aggregate_state} = state
 
     pending_events = List.wrap(pending_events)
@@ -521,8 +500,27 @@ defmodule Commanded.Aggregates.Aggregate do
           aggregate_version: aggregate_version
       }
 
-      {{:ok, aggregate_version, pending_events}, state}
+      {{:ok, pending_events}, state}
     else
+      {:error, :wrong_expected_version} ->
+        # Fetch missing events from event store
+        state = rebuild_from_events(state)
+
+        # Retry command if there are any attempts left
+        case ExecutionContext.retry(context) do
+          {:ok, context} ->
+            Logger.debug(fn -> describe(state) <> " wrong expected version, retrying command" end)
+
+            execute_command(context, state)
+
+          reply ->
+            Logger.debug(fn ->
+              describe(state) <> " wrong expected version, but not retrying command"
+            end)
+
+            {reply, state}
+        end
+
       {:error, _error} = reply ->
         {reply, state}
     end
diff --git a/lib/commanded/aggregates/execution_context.ex b/lib/commanded/aggregates/execution_context.ex
index dea0ce8..67797f1 100644
--- a/lib/commanded/aggregates/execution_context.ex
+++ b/lib/commanded/aggregates/execution_context.ex
@@ -32,8 +32,10 @@ defmodule Commanded.Aggregates.ExecutionContext do
 
   """
 
+  alias Commanded.Aggregates.Aggregate
   alias Commanded.Aggregates.DefaultLifespan
   alias Commanded.Aggregates.ExecutionContext
+  alias Commanded.Commands.ExecutionResult
 
   defstruct [
     :command,
@@ -42,6 +44,7 @@ defmodule Commanded.Aggregates.ExecutionContext do
     :function,
     :handler,
     retry_attempts: 0,
+    returning: false,
     lifespan: DefaultLifespan,
     metadata: %{}
   ]
@@ -59,4 +62,38 @@ defmodule Commanded.Aggregates.ExecutionContext do
 
     {:ok, context}
   end
+
+  def format_reply(reply, %ExecutionContext{} = context, %Aggregate{} = aggregate) do
+    %Aggregate{
+      aggregate_uuid: aggregate_uuid,
+      aggregate_state: aggregate_state,
+      aggregate_version: aggregate_version
+    } = aggregate
+
+    %ExecutionContext{metadata: metadata, returning: returning} = context
+
+    with {:ok, events} <- reply do
+      case returning do
+        :aggregate_state ->
+          {:ok, aggregate_version, events, aggregate_state}
+
+        :aggregate_version ->
+          {:ok, aggregate_version, events, aggregate_version}
+
+        :execution_result ->
+          result = %ExecutionResult{
+            aggregate_uuid: aggregate_uuid,
+            aggregate_state: aggregate_state,
+            aggregate_version: aggregate_version,
+            events: events,
+            metadata: metadata
+          }
+
+          {:ok, aggregate_version, events, result}
+
+        false ->
+          {:ok, aggregate_version, events}
+      end
+    end
+  end
 end
diff --git a/lib/commanded/commands/composite_router.ex b/lib/commanded/commands/composite_router.ex
index 6896fc0..bbd8854 100644
--- a/lib/commanded/commands/composite_router.ex
+++ b/lib/commanded/commands/composite_router.ex
@@ -1,20 +1,34 @@
 defmodule Commanded.Commands.CompositeRouter do
   @moduledoc """
   Composite router allows you to combine multiple router modules into a single
-  router able to dispatch any registered command using its corresponding router.
+  router able to dispatch any registered command from an included child router.
 
-  ## Example
+  One example usage is to define a router per context and then combine each
+  context's router into a single top-level composite app router used for all
+  command dispatching.
 
-      defmodule ExampleCompositeRouter do
+  ### Example
+
+  Define a composite router module which imports the commands from each included
+  router:
+
+      defmodule Bank.AppRouter do
         use Commanded.Commands.CompositeRouter
 
-        router BankAccountRouter
-        router MoneyTransferRouter
+        router(Bank.Accounts.Router)
+        router(Bank.MoneyTransfer.Router)
       end
 
+  You can dispatch a command via the composite router which will be routed to
+  the associated router:
+
+      alias Bank.AppRouter
+
       command = %OpenAccount{account_number: "ACC123", initial_balance: 1_000}
 
-      :ok = ExampleCompositeRouter.dispatch(command)
+      :ok = AppRouter.dispatch(command)
+
+  A composite router can include composite routers.
   """
 
   defmacro __using__(opts) do
@@ -23,8 +37,15 @@ defmodule Commanded.Commands.CompositeRouter do
 
       import unquote(__MODULE__)
 
+      application = Keyword.get(unquote(opts), :application)
+
+      default_dispatch_opts =
+        unquote(opts)
+        |> Keyword.get(:default_dispatch_opts, [])
+        |> Keyword.put(:application, application)
+
+      @default_dispatch_opts default_dispatch_opts
       @registered_commands %{}
-      @application Keyword.get(unquote(opts), :application)
 
       @before_compile unquote(__MODULE__)
     end
@@ -53,6 +74,10 @@ defmodule Commanded.Commands.CompositeRouter do
         Enum.map(@registered_commands, fn {command, _router} -> command end)
       end
 
+      def __dispatch_opts__(opts) do
+        Keyword.merge(@default_dispatch_opts, opts)
+      end
+
       @doc false
       def dispatch(command, opts \\ [])
 
@@ -62,18 +87,24 @@ defmodule Commanded.Commands.CompositeRouter do
           quote do
             @doc false
             def dispatch(%unquote(command_module){} = command, :infinity) do
-              unquote(router).dispatch(command, application: @application, timeout: :infinity)
+              opts = __dispatch_opts__(timeout: :infinity)
+
+              unquote(router).dispatch(command, opts)
             end
 
             @doc false
             def dispatch(%unquote(command_module){} = command, timeout)
                 when is_integer(timeout) do
-              unquote(router).dispatch(command, application: @application, timeout: timeout)
+              opts = __dispatch_opts__(timeout: timeout)
+
+              unquote(router).dispatch(command, opts)
             end
 
             @doc false
             def dispatch(%unquote(command_module){} = command, opts) do
-              unquote(router).dispatch(command, Keyword.put_new(opts, :application, @application))
+              opts = __dispatch_opts__(opts)
+
+              unquote(router).dispatch(command, opts)
             end
           end
         )
diff --git a/lib/commanded/commands/dispatcher.ex b/lib/commanded/commands/dispatcher.ex
index ef69ba1..eecb725 100644
--- a/lib/commanded/commands/dispatcher.ex
+++ b/lib/commanded/commands/dispatcher.ex
@@ -5,7 +5,6 @@ defmodule Commanded.Commands.Dispatcher do
 
   alias Commanded.Aggregates.Aggregate
   alias Commanded.Aggregates.ExecutionContext
-  alias Commanded.Commands.ExecutionResult
   alias Commanded.Middleware.Pipeline
 
   defmodule Payload do
@@ -21,29 +20,28 @@ defmodule Commanded.Commands.Dispatcher do
       :handler_module,
       :handler_function,
       :aggregate_module,
-      :include_aggregate_version,
-      :include_execution_result,
       :identity,
       :identity_prefix,
       :timeout,
       :lifespan,
       :metadata,
       :retry_attempts,
+      :returning,
       middleware: []
     ]
   end
 
   @doc """
-  Dispatch the given command to the handler module for the aggregate as identified
-
-  Returns `:ok`, `{:ok, aggregate_version}` or 
-  `{:ok, %Commanded.Commands.ExecutionResult{}}` on success, or `{:error, error}`
-  on failure.
+  Dispatch the given command to the handler module for the aggregate as
+  identified.
   """
-  @spec dispatch(payload :: struct) :: :ok |
-    {:ok, Commanded.Commands.ExecutionResult.t()} |
-    {:ok, aggregate_version :: integer()} |
-    {:error, error :: term}
+  @spec dispatch(payload :: struct) ::
+          :ok
+          | {:ok, aggregate_state :: struct}
+          | {:ok, aggregate_version :: non_neg_integer()}
+          | {:ok, events :: list(struct)}
+          | {:ok, Commanded.Commands.ExecutionResult.t()}
+          | {:error, error :: term}
   def dispatch(%Payload{} = payload) do
     pipeline =
       payload
@@ -105,7 +103,14 @@ defmodule Commanded.Commands.Dispatcher do
         |> Pipeline.assign(:aggregate_version, aggregate_version)
         |> Pipeline.assign(:events, events)
         |> after_dispatch(payload)
-        |> respond_with_success(payload, events)
+        |> Pipeline.respond(:ok)
+
+      {:ok, aggregate_version, events, reply} ->
+        pipeline
+        |> Pipeline.assign(:aggregate_version, aggregate_version)
+        |> Pipeline.assign(:events, events)
+        |> after_dispatch(payload)
+        |> Pipeline.respond({:ok, reply})
 
       {:exit, {:normal, :aggregate_stopped}} ->
         # Maybe retry command when aggregate process stopped by lifespan timeout
@@ -138,7 +143,8 @@ defmodule Commanded.Commands.Dispatcher do
       handler_module: handler_module,
       handler_function: handler_function,
       lifespan: lifespan,
-      retry_attempts: retry_attempts
+      retry_attempts: retry_attempts,
+      returning: returning
     } = payload
 
     %ExecutionContext{
@@ -149,34 +155,11 @@ defmodule Commanded.Commands.Dispatcher do
       handler: handler_module,
       function: handler_function,
       lifespan: lifespan,
-      retry_attempts: retry_attempts
+      retry_attempts: retry_attempts,
+      returning: returning
     }
   end
 
-  defp respond_with_success(%Pipeline{} = pipeline, payload, events) do
-    response =
-      case payload do
-        %{include_execution_result: true} ->
-          {
-            :ok,
-            %ExecutionResult{
-              aggregate_uuid: pipeline.assigns.aggregate_uuid,
-              aggregate_version: pipeline.assigns.aggregate_version,
-              events: events,
-              metadata: pipeline.metadata
-            }
-          }
-
-        %{include_aggregate_version: true} ->
-          {:ok, pipeline.assigns.aggregate_version}
-
-        _ ->
-          :ok
-      end
-
-    Pipeline.respond(pipeline, response)
-  end
-
   defp before_dispatch(%Pipeline{} = pipeline, %Payload{middleware: middleware}) do
     Pipeline.chain(pipeline, :before_dispatch, middleware)
   end
diff --git a/lib/commanded/commands/execution_result.ex b/lib/commanded/commands/execution_result.ex
index acc7a8e..81a07e2 100644
--- a/lib/commanded/commands/execution_result.ex
+++ b/lib/commanded/commands/execution_result.ex
@@ -1,30 +1,40 @@
 defmodule Commanded.Commands.ExecutionResult do
   @moduledoc """
-  Contains the events and metadata created by a command succesfully executed
-  against an aggregate.
+  Contains the aggregate, events, and metadata created by a successfully
+  executed command.
 
   The available fields are:
 
-  - `aggregate_uuid` - identity of the aggregate instance.
-  - `aggregate_version` - resultant version of the aggregate after executing
-    the command.
-  - `events` - a list of the created events, may be an empty list.
-  - `metadata` - an optional map containing the metadata associated with the
-    command dispatch.
+    - `aggregate_uuid` - identity of the aggregate instance.
+
+    - `aggregate_state` - resultant state of the aggregate after executing
+      the command.
+
+    - `aggregate_version` - resultant version of the aggregate after executing
+      the command.
+
+    - `events` - a list of the created events, it may be an empty list.
+
+    - `metadata` - an map containing the metadata associated with the command
+      dispatch.
 
   """
 
   @type t :: %__MODULE__{
-    aggregate_uuid: String.t,
-    aggregate_version: non_neg_integer(),
-    events: list(struct()),
-    metadata: struct(),
-  }
+          aggregate_uuid: String.t(),
+          aggregate_state: struct,
+          aggregate_version: non_neg_integer(),
+          events: list(struct()),
+          metadata: struct()
+        }
+
+  @enforce_keys [:aggregate_uuid, :aggregate_state, :aggregate_version, :events, :metadata]
 
   defstruct [
-    aggregate_uuid: nil,
-    aggregate_version: nil,
-    events: nil,
-    metadata: nil,
+    :aggregate_uuid,
+    :aggregate_state,
+    :aggregate_version,
+    :events,
+    :metadata
   ]
 end
diff --git a/lib/commanded/commands/router.ex b/lib/commanded/commands/router.ex
index 7a76f42..acde444 100644
--- a/lib/commanded/commands/router.ex
+++ b/lib/commanded/commands/router.ex
@@ -119,30 +119,64 @@ defmodule Commanded.Commands.Router do
       Note you cannot opt-in to strong consistency for a handler that has been
       configured as eventually consistent.
 
-  ## Aggregate version
+  ## Dispatch return
 
-  You can optionally choose to include the aggregate's version as part of the
-  dispatch result by setting `include_aggregate_version` true.
+  By default a successful command dispatch will return `:ok`. You can change
+  this behaviour by specifying a `returning` option.
 
-      {:ok, aggregate_version} = BankApp.dispatch(command, include_aggregate_version: true)
+  The supported options are:
 
-  This is useful when you need to wait for an event handler (e.g. a read model
-  projection) to be up-to-date before continuing or querying its data.
+    - `:aggregate_state` - to return the update aggregate state.
 
-  ## Execution results
+    - `:aggregate_version` - to return only the aggregate version.
 
-  You can also choose to include the execution result as part of the dispatch result by
-  setting `include_execution_result` true.
+    - `:execution_result` - to return a `Commanded.Commands.ExecutionResult`
+      struct containing the aggregate's identity, state, version, and any events
+      produced from the command along with their associated metadata.
 
-      {:ok, execution_result} = BankApp.dispatch(command, include_execution_result: true)
+    - `false` - don't return anything except an `:ok`.
 
-  Or by setting `include_execution_result` in your application config file:
+  ### Aggregate state
+
+  Return the updated aggregate state as part of the dispatch result:
+
+      {:ok, %BankAccount{}} = BankApp.dispatch(command, returning: :aggregate_state)
+
+  This is useful when you want to immediately return fields from the aggregate's
+  state without requiring an read model projection and waiting for the event(s)
+  to be projected. It may also be appropriate to use this feature for unit
+  tests.
+
+  However, be warned that tightly coupling an aggregate's state with read
+  requests may be harmful. It's why CQRS enforces the separation of reads from
+  writes by defining two separate and specialised models.
+
+  ### Aggregate version
+
+  You can optionally choose to return the aggregate's version as part of the
+  dispatch result:
+
+      {:ok, aggregate_version} = BankApp.dispatch(command, returning: :aggregate_version)
+
+  This is useful when you need to wait for an event handler, such as a read model
+  projection, to be up-to-date before querying its data.
+
+  ### Execution results
+
+  You can also choose to return the execution result as part of the dispatch
+  result:
+
+      alias Commanded.Commands.ExecutionResult
+
+      {:ok, %ExecutionResult{} = result} = BankApp.dispatch(command, returning: :execution_result)
+
+  Or by setting the `default_dispatch_return` in your application config file:
 
       # config/config.exs
-      config :commanded, include_execution_result: true
+      config :commanded, default_dispatch_return: :execution_result
 
-  Use this if you need to get information from the events produced by the aggregate
-  but you don't want to wait for the events to be projected.
+  Use the execution result struct to get information from the events produced
+  from the command.
 
   ## Metadata
 
@@ -174,9 +208,8 @@ defmodule Commanded.Commands.Router do
           Commanded.Middleware.ExtractAggregateIdentity,
           Commanded.Middleware.ConsistencyGuarantee
         ],
-        consistency: get_env(:default_consistency, :eventual),
-        include_aggregate_version: get_env(:include_aggregate_version, false),
-        include_execution_result: get_env(:include_execution_result, false),
+        consistency: get_opt(unquote(opts), :default_consistency, :eventual),
+        returning: get_default_dispatch_return(unquote(opts)),
         dispatch_timeout: 5_000,
         lifespan: Commanded.Aggregates.DefaultLifespan,
         metadata: %{},
@@ -195,15 +228,16 @@ defmodule Commanded.Commands.Router do
 
   ## Example
 
-      defmodule BankingRouter do
+      defmodule BankRouter do
         use Commanded.Commands.Router
 
         middleware CommandLogger
         middleware MyCommandValidator
         middleware AuthorizeCommand
 
-        dispatch [OpenAccount,DepositMoney] to: BankAccount, identity: :account_number
+        dispatch [OpenAccount, DepositMoney], to: BankAccount, identity: :account_number
       end
+
   """
   defmacro middleware(middleware_module) do
     quote do
@@ -283,6 +317,15 @@ defmodule Commanded.Commands.Router do
   @doc """
   Configure the command, or list of commands, to be dispatched to the
   corresponding handler for a given aggregate.
+
+  ## Example
+
+      defmodule BankRouter do
+        use Commanded.Commands.Router
+
+        dispatch [OpenAccount, DepositMoney], to: BankAccount, identity: :account_number
+      end
+
   """
   defmacro dispatch(command_module_or_modules, opts) do
     opts = parse_opts(opts, [])
@@ -402,11 +445,25 @@ defmodule Commanded.Commands.Router do
         metadata = Keyword.get(opts, :metadata) || @default[:metadata]
         timeout = Keyword.get(opts, :timeout) || unquote(timeout) || @default[:dispatch_timeout]
 
-        include_aggregate_version =
-          Keyword.get(opts, :include_aggregate_version) || @default[:include_aggregate_version]
+        returning =
+          cond do
+            (returning = Keyword.get(opts, :returning)) in [
+              :aggregate_state,
+              :aggregate_version,
+              :execution_result,
+              false
+            ] ->
+              returning
+
+            Keyword.get(opts, :include_execution_result) == true ->
+              :execution_result
+
+            Keyword.get(opts, :include_aggregate_version) == true ->
+              :aggregate_version
 
-        include_execution_result =
-          Keyword.get(opts, :include_execution_result) || @default[:include_execution_result]
+            true ->
+              @default[:returning]
+          end
 
         lifespan = Keyword.get(opts, :lifespan) || unquote(lifespan) || @default[:lifespan]
         retry_attempts = Keyword.get(opts, :retry_attempts) || @default[:retry_attempts]
@@ -438,8 +495,7 @@ defmodule Commanded.Commands.Router do
           aggregate_module: unquote(aggregate),
           identity: identity,
           identity_prefix: identity_prefix,
-          include_aggregate_version: include_aggregate_version,
-          include_execution_result: include_execution_result,
+          returning: returning,
           timeout: timeout,
           lifespan: lifespan,
           metadata: metadata,
@@ -464,8 +520,9 @@ defmodule Commanded.Commands.Router do
       """
       @spec dispatch(command :: struct) ::
               :ok
+              | {:ok, aggregate_state :: struct}
+              | {:ok, aggregate_version :: non_neg_integer()}
               | {:ok, execution_result :: Commanded.Commands.ExecutionResult.t()}
-              | {:ok, aggregate_version :: integer}
               | {:error, :unregistered_command}
               | {:error, :consistency_timeout}
               | {:error, reason :: term}
@@ -496,31 +553,41 @@ defmodule Commanded.Commands.Router do
             will block until all strongly consistent event handlers and process
             managers have handled all events created by the command.
 
-          - `timeout` - as described above.
-
-          - `include_aggregate_version` - set to true to include the aggregate
-            stream version in the success response: `{:ok, aggregate_version}`
-            The default is false, to return just `:ok`.
-
-          - `include_execution_result` - set to true to include more
-            information about the dispatch, like the aggregate name, uuid, and
-            the produced events. Overrides `include_aggregate_version`. The
-            default is false to return `:ok`. See
-            `Commanded.Commands.ExecutionResult`.
-
           - `metadata` - an optional map containing key/value pairs comprising
             the metadata to be associated with all events created by the
             command.
 
-      Returns `:ok` on success, unless `:include_aggregate_version` or
-      `:include_execution_result` is enabled, where it respectively returns
-      `{:ok, aggregate_version}` or `{:ok, %ExecutionResult{..}}`. Returns
-      `{:error, reason}` on failure.
+          - `returning` - to choose what response is returned from a successful
+              command dispatch. The default is to return an `:ok`.
+
+              The available options are:
+
+              - `:aggregate_state` - to return the update aggregate state in the
+                successful response: `{:ok, aggregate_state}`.
+
+              - `:aggregate_version` - to include the aggregate stream version
+                in the successful response: `{:ok, aggregate_version}`.
+
+              - `:execution_result` - to return a `Commanded.Commands.ExecutionResult`
+                struct containing the aggregate's identity, version, and any
+                events produced from the command along with their associated
+                metadata.
+
+              - `false` - don't return anything except an `:ok`.
+
+          - `timeout` - as described above.
+
+      Returns `:ok` on success unless the `:returning` option is specified where
+      it returns one of `{:ok, aggregate_state}`, `{:ok, aggregate_version}`, or
+      `{:ok, %Commanded.Commands.ExecutionResult{}}`.
+
+      Returns `{:error, reason}` on failure.
       """
       @spec dispatch(command :: struct, timeout_or_opts :: integer | :infinity | keyword()) ::
               :ok
+              | {:ok, aggregate_state :: struct}
+              | {:ok, aggregate_version :: non_neg_integer()}
               | {:ok, execution_result :: Commanded.Commands.ExecutionResult.t()}
-              | {:ok, aggregate_version :: integer}
               | {:error, :unregistered_command}
               | {:error, :consistency_timeout}
               | {:error, reason :: term}
@@ -545,7 +612,31 @@ defmodule Commanded.Commands.Router do
   end
 
   @doc false
-  def get_env(name, default \\ nil), do: Application.get_env(:commanded, name, default)
+  def get_opt(opts, name, default \\ nil) do
+    Keyword.get(opts, name) || Application.get_env(:commanded, name) || default
+  end
+
+  @doc false
+  def get_default_dispatch_return(opts) do
+    cond do
+      (default_dispatch_return = get_opt(opts, :default_dispatch_return)) in [
+        :aggregate_state,
+        :aggregate_version,
+        :execution_result,
+        false
+      ] ->
+        default_dispatch_return
+
+      get_opt(opts, :include_execution_result) == true ->
+        :execution_result
+
+      get_opt(opts, :include_aggregate_version) == true ->
+        :aggregate_version
+
+      true ->
+        false
+    end
+  end
 
   defp parse_opts([{:to, aggregate_or_handler} | opts], result) do
     case Keyword.pop(opts, :aggregate) do
diff --git a/lib/commanded/process_managers/process_manager_instance.ex b/lib/commanded/process_managers/process_manager_instance.ex
index 4a76591..fbc0705 100644
--- a/lib/commanded/process_managers/process_manager_instance.ex
+++ b/lib/commanded/process_managers/process_manager_instance.ex
@@ -183,7 +183,7 @@ defmodule Commanded.ProcessManagers.ProcessManagerInstance do
 
       commands ->
         # Copy event id, as causation id, and correlation id from handled event.
-        opts = [causation_id: event_id, correlation_id: correlation_id]
+        opts = [causation_id: event_id, correlation_id: correlation_id, returning: false]
 
         with :ok <- commands |> List.wrap() |> dispatch_commands(opts, state, event) do
           process_state = mutate_state(event, state)
@@ -300,10 +300,6 @@ defmodule Commanded.ProcessManagers.ProcessManagerInstance do
       :ok ->
         dispatch_commands(pending_commands, opts, state, last_event)
 
-      # 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 ->
         Logger.warn(fn ->
           describe(state) <>


Test patch:
diff --git a/test/aggregates/aggregate_concurrency_test.exs b/test/aggregates/aggregate_concurrency_test.exs
index 5dcbdfd..33fdc6e 100644
--- a/test/aggregates/aggregate_concurrency_test.exs
+++ b/test/aggregates/aggregate_concurrency_test.exs
@@ -96,10 +96,8 @@ defmodule Commanded.Aggregates.AggregateConcurrencyTest do
              }
     end
 
-    test "should error after too many attempts", context do
-      %{account_number: account_number} = context
-
-      # fail to append to stream
+    test "should error after too many attempts", %{account_number: account_number} do
+      # Fail to append to stream
       expect(MockEventStore, :append_to_stream, 6, fn _event_store_meta,
                                                       ^account_number,
                                                       1,
diff --git a/test/aggregates/execute_command_test.exs b/test/aggregates/execute_command_test.exs
index 090c895..9af0fca 100644
--- a/test/aggregates/execute_command_test.exs
+++ b/test/aggregates/execute_command_test.exs
@@ -26,7 +26,7 @@ defmodule Commanded.Aggregates.ExecuteCommandTest do
 
     {:ok, 1, events} = Aggregate.execute(BankApp, BankAccount, account_number, context)
 
-    assert events == [%BankAccountOpened{account_number: account_number, initial_balance: 1000}]
+    assert events == [%BankAccountOpened{account_number: account_number, initial_balance: 1_000}]
 
     shutdown_aggregate(BankApp, BankAccount, account_number)
 
@@ -52,11 +52,11 @@ defmodule Commanded.Aggregates.ExecuteCommandTest do
 
     {:ok, 1, events} = Aggregate.execute(BankApp, BankAccount, account_number, context)
 
-    assert events == [%BankAccountOpened{account_number: account_number, initial_balance: 1000}]
+    assert events == [%BankAccountOpened{account_number: account_number, initial_balance: 1_000}]
 
     shutdown_aggregate(BankApp, BankAccount, account_number)
 
-    # reload aggregate to fetch persisted events from event store and rebuild state by applying saved events
+    # Reload aggregate to fetch persisted events from event store and rebuild state by applying saved events
     {:ok, ^account_number} = open_aggregate(BankAccount, account_number)
 
     assert Aggregate.aggregate_version(BankApp, BankAccount, account_number) == 1
diff --git a/test/commands/dispatch_consistency_test.exs b/test/commands/dispatch_consistency_test.exs
index 04d00de..ec65e87 100644
--- a/test/commands/dispatch_consistency_test.exs
+++ b/test/commands/dispatch_consistency_test.exs
@@ -23,7 +23,6 @@ defmodule Commanded.Commands.DispatchConsistencyTest do
   setup do
     start_supervised!(DefaultApp)
     start_supervised!(ConsistencyApp)
-
     :ok
   end
 
diff --git a/test/commands/dispatch_return_test.exs b/test/commands/dispatch_return_test.exs
new file mode 100644
index 0000000..d2f8ef7
--- /dev/null
+++ b/test/commands/dispatch_return_test.exs
@@ -0,0 +1,187 @@
+defmodule Commanded.Commands.DispatchReturnTest do
+  use Commanded.StorageCase
+
+  alias Commanded.Commands.ExecutionResult
+  alias Commanded.ExampleDomain.BankApp
+  alias Commanded.ExampleDomain.BankAccount
+  alias Commanded.ExampleDomain.BankAccount.Commands.DepositMoney
+  alias Commanded.ExampleDomain.BankAccount.Commands.OpenAccount
+  alias Commanded.ExampleDomain.BankAccount.Events.BankAccountOpened
+  alias Commanded.Helpers.CommandAuditMiddleware
+
+  setup do
+    start_supervised!(BankApp)
+    start_supervised!(CommandAuditMiddleware)
+    :ok
+  end
+
+  describe "dispatch return disabled" do
+    test "should return `:ok`" do
+      command = %OpenAccount{account_number: "ACC123", initial_balance: 1_000}
+
+      assert :ok == BankApp.dispatch(command, returning: false)
+    end
+
+    test "should return an error on failure" do
+      command = %OpenAccount{account_number: "ACC123", initial_balance: -1}
+
+      assert {:error, :invalid_initial_balance} ==
+               BankApp.dispatch(command, returning: false)
+    end
+  end
+
+  describe "dispatch return aggregate state" do
+    test "should return aggregate's updated state" do
+      assert {:ok, %BankAccount{account_number: "ACC123", balance: 1_000, state: :active}} ==
+               BankApp.dispatch(
+                 %OpenAccount{account_number: "ACC123", initial_balance: 1_000},
+                 returning: :aggregate_state
+               )
+
+      assert {:ok, %BankAccount{account_number: "ACC123", balance: 1_100, state: :active}} ==
+               BankApp.dispatch(
+                 %DepositMoney{account_number: "ACC123", amount: 100},
+                 returning: :aggregate_state
+               )
+    end
+
+    test "should return an error on failure" do
+      command = %OpenAccount{account_number: "ACC123", initial_balance: -1}
+
+      assert {:error, :invalid_initial_balance} ==
+               BankApp.dispatch(command, returning: :aggregate_state)
+    end
+  end
+
+  describe "dispatch return aggregate version" do
+    test "should return aggregate's updated version" do
+      assert {:ok, 1} ==
+               BankApp.dispatch(
+                 %OpenAccount{account_number: "ACC123", initial_balance: 1_000},
+                 returning: :aggregate_version
+               )
+
+      assert {:ok, 2} ==
+               BankApp.dispatch(
+                 %DepositMoney{account_number: "ACC123", amount: 100},
+                 returning: :aggregate_version
+               )
+    end
+
+    test "should return an error on failure" do
+      command = %OpenAccount{account_number: "ACC123", initial_balance: -1}
+
+      assert {:error, :invalid_initial_balance} ==
+               BankApp.dispatch(command, returning: :aggregate_version)
+    end
+  end
+
+  describe "dispatch return execution result" do
+    test "should return created events" do
+      metadata = %{"ip_address" => "127.0.0.1"}
+      command = %OpenAccount{account_number: "ACC123", initial_balance: 1_000}
+
+      assert BankApp.dispatch(command, metadata: metadata, returning: :execution_result) ==
+               {
+                 :ok,
+                 %ExecutionResult{
+                   aggregate_uuid: "ACC123",
+                   aggregate_state: %BankAccount{
+                     account_number: "ACC123",
+                     balance: 1_000,
+                     state: :active
+                   },
+                   aggregate_version: 1,
+                   events: [%BankAccountOpened{account_number: "ACC123", initial_balance: 1_000}],
+                   metadata: metadata
+                 }
+               }
+    end
+
+    test "should return an error on failure" do
+      command = %OpenAccount{account_number: "ACC123", initial_balance: -1}
+
+      assert {:error, :invalid_initial_balance} ==
+               BankApp.dispatch(command, returning: :execution_result)
+    end
+  end
+
+  describe "dispatch include aggregate version" do
+    test "should return aggregate's updated version" do
+      assert {:ok, 1} ==
+               BankApp.dispatch(
+                 %OpenAccount{account_number: "ACC123", initial_balance: 1_000},
+                 include_aggregate_version: true
+               )
+
+      assert {:ok, 2} ==
+               BankApp.dispatch(
+                 %DepositMoney{account_number: "ACC123", amount: 100},
+                 include_aggregate_version: true
+               )
+    end
+  end
+
+  describe "dispatch include execution result" do
+    test "should return created events" do
+      metadata = %{"ip_address" => "127.0.0.1"}
+      command = %OpenAccount{account_number: "ACC123", initial_balance: 1_000}
+
+      assert BankApp.dispatch(command,
+               metadata: metadata,
+               include_execution_result: true
+             ) ==
+               {
+                 :ok,
+                 %ExecutionResult{
+                   aggregate_uuid: "ACC123",
+                   aggregate_state: %BankAccount{
+                     account_number: "ACC123",
+                     balance: 1_000,
+                     state: :active
+                   },
+                   aggregate_version: 1,
+                   events: [%BankAccountOpened{account_number: "ACC123", initial_balance: 1_000}],
+                   metadata: metadata
+                 }
+               }
+    end
+  end
+
+  describe "application dispatch return aggregate state" do
+    alias Commanded.Commands.DefaultDispatchReturnApp
+
+    setup do
+      start_supervised!(DefaultDispatchReturnApp)
+      :ok
+    end
+
+    test "should return aggregate's updated version" do
+      assert {:ok, 1} ==
+               DefaultDispatchReturnApp.dispatch(%OpenAccount{
+                 account_number: "ACC123",
+                 initial_balance: 1_000
+               })
+
+      assert {:ok, 2} ==
+               DefaultDispatchReturnApp.dispatch(%DepositMoney{
+                 account_number: "ACC123",
+                 amount: 100
+               })
+    end
+
+    test "should allow default to be overridden during dispatch" do
+      assert {:ok, 1} ==
+               DefaultDispatchReturnApp.dispatch(%OpenAccount{
+                 account_number: "ACC123",
+                 initial_balance: 1_000
+               })
+
+      assert {:ok, 2} ==
+               DefaultDispatchReturnApp.dispatch(%DepositMoney{
+                 account_number: "ACC123",
+                 amount: 100
+               })
+    end
+  end
+end
diff --git a/test/commands/routing_commands_test.exs b/test/commands/routing_commands_test.exs
index b8ceea2..152cb6e 100644
--- a/test/commands/routing_commands_test.exs
+++ b/test/commands/routing_commands_test.exs
@@ -2,19 +2,16 @@ defmodule Commanded.Commands.RoutingCommandsTest do
   use Commanded.StorageCase
 
   alias Commanded.DefaultApp
-  alias Commanded.Commands.{ExecutionResult, UnregisteredCommand}
+  alias Commanded.Commands.UnregisteredCommand
   alias Commanded.EventStore
   alias Commanded.ExampleDomain.BankAccount
-  alias Commanded.ExampleDomain.{OpenAccountHandler, DepositMoneyHandler, WithdrawMoneyHandler}
-
-  alias Commanded.ExampleDomain.BankAccount.Commands.{
-    OpenAccount,
-    CloseAccount,
-    DepositMoney,
-    WithdrawMoney
-  }
-
-  alias Commanded.ExampleDomain.BankAccount.Events.BankAccountOpened
+  alias Commanded.ExampleDomain.OpenAccountHandler
+  alias Commanded.ExampleDomain.DepositMoneyHandler
+  alias Commanded.ExampleDomain.WithdrawMoneyHandler
+  alias Commanded.ExampleDomain.BankAccount.Commands.OpenAccount
+  alias Commanded.ExampleDomain.BankAccount.Commands.CloseAccount
+  alias Commanded.ExampleDomain.BankAccount.Commands.DepositMoney
+  alias Commanded.ExampleDomain.BankAccount.Commands.WithdrawMoney
 
   @dispatch_opts [application: DefaultApp]
 
@@ -368,24 +365,6 @@ defmodule Commanded.Commands.RoutingCommandsTest do
              )
   end
 
-  describe "include aggregate version" do
-    test "should return aggregate's updated stream version" do
-      assert {:ok, 1} ==
-               MultiCommandHandlerRouter.dispatch(
-                 %OpenAccount{account_number: "ACC123", initial_balance: 1_000},
-                 application: DefaultApp,
-                 include_aggregate_version: true
-               )
-
-      assert {:ok, 2} ==
-               MultiCommandHandlerRouter.dispatch(
-                 %DepositMoney{account_number: "ACC123", amount: 100},
-                 application: DefaultApp,
-                 include_aggregate_version: true
-               )
-    end
-  end
-
   test "should allow setting metadata" do
     metadata = %{"ip_address" => "127.0.0.1"}
 
@@ -410,26 +389,4 @@ defmodule Commanded.Commands.RoutingCommandsTest do
       assert event.metadata == metadata
     end)
   end
-
-  describe "include execution result" do
-    test "should return created events" do
-      metadata = %{"ip_address" => "127.0.0.1"}
-      command = %OpenAccount{account_number: "ACC123", initial_balance: 1_000}
-
-      assert MultiCommandHandlerRouter.dispatch(command,
-               application: DefaultApp,
-               metadata: metadata,
-               include_execution_result: true
-             ) ==
-               {
-                 :ok,
-                 %ExecutionResult{
-                   aggregate_uuid: "ACC123",
-                   aggregate_version: 1,
-                   events: [%BankAccountOpened{account_number: "ACC123", initial_balance: 1000}],
-                   metadata: metadata
-                 }
-               }
-    end
-  end
 end
diff --git a/test/commands/support/returning/default_dispatch_return_app.ex b/test/commands/support/returning/default_dispatch_return_app.ex
new file mode 100644
index 0000000..d82cbe9
--- /dev/null
+++ b/test/commands/support/returning/default_dispatch_return_app.ex
@@ -0,0 +1,18 @@
+defmodule Commanded.Commands.DefaultDispatchReturnApp do
+  alias Commanded.ExampleDomain.BankRouter
+
+  use Commanded.Application,
+    otp_app: :commanded,
+    event_store: [
+      adapter: Commanded.EventStore.Adapters.InMemory,
+      serializer: Commanded.Serialization.JsonSerializer
+    ],
+    pubsub: :local,
+    registry: :local,
+    default_dispatch_opts: [
+      consistency: :eventual,
+      returning: :aggregate_version
+    ]
+
+  router(BankRouter)
+end