Elixir CQRS/ES Commanded
When deciding how to architect an Elixir application which uses Commanded you can choose the most suitable design from one of the following options.
- One monolithic service with a single global Commanded application.
- One monolithic service containing many contexts, all sharing a single Commanded application.
- One monolithic service containing many contexts, each context using its own Commanded application.
- Many microservices with each service using its own Commanded application, and an additional Commanded application for integration.
- Multi-tenant service with each tenant using its own Commanded Application.
Monolithic service, global Commanded Application
This is the simplest approach where one service defines a single global Commanded Application and EventStore which is connected to a single Postgres database.
- Single “majestic monolith” service.
- Single global Commanded Application.
- Single global EventStore connected to a Postgres database.
You define a single Commanded Application module and a single EventStore module:
The Commanded Application (
MyApp.App) is included in the Elixir application’s supervision tree:
Once configured and started you can use the Commanded Application to dispatch commands:
Or use it in an event handler to process events:
Multi-context monolithic service, shared Commanded Application
- Single monolithic service, partitioned by context.
- Single global Commanded Application shared by each context.
- Single global EventStore connected to a Postgres database shared by each context.
Here we split our service into smaller contexts, but still using a global Commanded Application and EventStore. The events may be consumed by any context within the service as it uses a single shared event store. This may ease development at the expense of tighter coupling and contention as contexts may need to process all events, not just their own.
In this example the monolith is designed in a modular style with multiple contexts each sharing a single Commanded Application and EventStore. The shared Commanded Application includes the individual router modules defined in each context allowing the dispatch of any command registered in any context.
In the Elixir application supervisor we would include the Commanded Application and the supervisors belonging to each context:
Multi-context monolithic service, separate Commanded Applications and database schema
- Single monolithic service.
- Multiple Commanded Applications - one for each context.
- Multiple EventStores - one for each context - connected to a single Postgres database segregated by schema.
The contexts within the monolithic service are further isolated by defining a separate Commanded Application and EventStore for each context. The EventStore for each context will be configured to use its own, separate database schema so that the events are stored separately. This guarantees isolation between contexts, ensuring that event handling within individual contexts do not affect each other.
We define a separate
Commanded.Application module and
EventStore module for each context. Each EventStore module would be configured to use its own Postgres database schema, specific to each context. The following example shows the modules for context A. The other contexts would define similar modules themselves.
Each context’s Commanded Application would be then supervised:
Usually you would define a top-level
Supervisor module for each context which would include the Commanded Application.
Commands would then be dispatched using the specific Commanded Application for the context. This would be
MyApp.ContextA.App for context A, etc.
It would not be possible for context A to dispatch a command for a different context without using the other context’s own Commanded Application. Instead a context should define public functions in a
MyApp.ContextA module to expose any operations to be used by other contexts. Internally, these functions can use the context’s own Commanded Application, hiding this information from the caller as an implementation detail.
Similarly, an event handler will only process the events for the context relating to the Commanded Application it uses. In the following example the handler would only receive events from context A.
Multi-context monolithic service, separate Commanded Applications and database
- Single monolithic service.
- Multiple Commanded Applications - one for each context.
- Multiple EventStores and Postgres databases - one for each context.
This is almost identical to the previous example, except the configuration for each context’s EventStore would connect to a different Postgres database rather than using a different schema within the same database. Otherwise the usage is the same as before with each context having its own Commanded Application module and EventStore module.
Define a Commanded Application module and EventStore module for each context:
Configure the event store for each context to connect to its own Postgres database:
- One service per context.
- One Commanded Application per service.
- One EventStore and Postgres database per service.
- Additional Commanded Application and EventStore used for inter-service messaging and integration.
In the microservices deployment model each service would define its own Commanded Application and EventStore. In addition to these, another shared Commanded Application and EventStore could be used for any inter-service messaging. With this architecture each service is autonomous and can be independently scaled, deployed, and upgraded.
To allow inter-service communication a shared event store is used. With this approach each service can choose to publish a subset of its internal private domain events to the shared public integration event store. These public integration events can then be consumed by any other service.
The services could also be integrated using another mechanism, such as with an enterprise service bus, message queue, etc.
Define a Commanded Application and EventStore for each individual service:
Additionally, define a shared Commanded Application and EventStore used by all services:
Within each service the supervision tree would need to include both the service’s own Commanded Application and the shared integration Commanded Application:
Any service can use the integration Commanded Application to publish a public integration event. This is done by appending one or more events to a named stream. In this example a
MyApp.Integration.Event event struct is used and it contains the type of the event and its data. A single integration event struct is used for all integration events so that each service does not need to have a dependency on one another nor share its own event modules. It allows dynamic event types to be published and consumers can opt-in to process only the events they are interested in and are able to process.
The purpose of using
Commanded.EventStore to append events, instead of using the EventStore module directly, is because it acts as a facade to use whatever event store is configured for the Commanded Application. For instance, this allows the in-memory event store to be configured for testing and the Postgres EventStore to be used in development and production.
An event handler can be used in any service to process these public integration events:
- Single or multiple services and/or contexts.
- Multiple Commanded Application instances - one per tenant.
- Multiple EventStore instances - one per tenant - connected to a separate Postgres database or schema.
Any of the previous architectures can be augmented with a multi-tenant approach by running multiple instances of any Commanded Application and its EventStore. One instance is started for each tenant. Each instance will use its own isolated event store, either segregated by Postgres schema or using a separate database.
The benefit of this architecture is that each tenant’s data is stored in its own event store, completely isolated from one another. It also allows concurrent processing of each tenant’s data since event handlers run for each tenant, processing events for that single tenant only. One large tenant will not negatively impact other tenants.
There is additional complexity with this approach as each tenant must be supervised and have its own event store database or schema provisioned.
First we define a Commanded Application module and EventStore module. The Commanded Application uses the
init/1 callback function to configure the event store for the tenant that is provided when it starts. This means each tenant-specific Commanded Application instance will use its own event store instance connected to its own Postgres database or schema.
Then we start one instance of the Commanded Application for each tenant.
Included in the supervision tree above for each tenant was an event handler which specifies the Commanded Application at runtime. This is to allow multiple instances of the handler to run, one instance for each tenant. The event handler will only process events for a single tenant.