Explore how Elixir's Commanded library revolutionizes application development through CQRS and Event Sourcing, offering powerful capabilities for audit, time travel, and seamless event-driven architecture.

Table of contents

    Responsibility segregation in Elixir programming language

    CQRS

    Command Query Responsibility Segregation (CQRS) at a conceptual level, emphasizes that actions leading to state changes are designated as commands, while data retrieval actions are termed queries. Due to the distinct operational demands of executing commands and queries, developers are encouraged to employ diverse persistence strategies for handling each, thus segregating their responsibilities.

    Event Sourcing pattern

    Event Sourcing is a pattern for storing data as events in an append-only log called event store. While this definition may seem straightforward, it overlooks a crucial aspect: by storing events, the context surrounding each event is retained. For instance, from a single piece of information, you can discern not only that an email was sent but also the reason behind it. In contrast, in alternative storage patterns, the context of business logic operations is often lost or stored separately. The current state of an entity can be reconstructed by sequentially replaying all events in the order they occurred. The information within the system is derived directly from these events.

    The proposal of the CQRS pattern coincided with the introduction of Event Sourcing to the public intentionally. Unlike state-based persistence, where circumventing the use of the domain model for queries may be feasible, such an approach becomes challenging, if not impossible, in event-sourced systems. This is due to the absence of a singular location where the complete state of a domain object is stored.

    Pros of event sourced system

    What are the benefits of those? Apart from others:

    • Audit: Data is stored as immutable events, providing a robust audit event log.
    • Time Travel: All state changes are retained, allowing for backward and forward time traversal, beneficial for debugging or analysis, and enabling the rebuilding of downstream projections.
    • Root Cause Analysis: Business events can be traced back to their origins, offering visibility into entire workflows.
    • Event-Driven Architecture: Immediate reaction to new events enhances efficiency and enables real-time business workflow modeling.
    • Asynchronous First: Minimizing synchronous interactions leads to responsive, high-performance, scalable systems.
    • One-Way Data Flow: Data flows one way through independent models, aiding in reasoning and debugging.

    Eventual consistency

    Eventual consistency entails the concept that reads or queries may not be instantly synchronized with writes. This phenomenon can also arise in systems that do not utilize event sourcing, often due to database replication delays. In essence, the data in your read storage eventually catches up with the latest writes, which could take milliseconds, seconds, minutes, etc. Given this, event sourcing systems frequently encounter eventual consistency challenges, necessitating readiness to address the trade-offs between eventual and strong consistency.

    Commanded facilitates the implementation of patterns in Elixir apps

    In the context of Elixir programming, the Commanded package provides a powerful framework for implementing CQRS and Event Sourcing patterns. Let's explore how Commanded facilitates the implementation of these patterns in Elixir applications.

    Commanded in practice

    Let's explore the application code and check how to implement event sourcing in our own elixir applications

    As always when adding a new package we need to adjust our mix.exs file:

    def deps do
      [
          {:commanded, "~> 1.4"},
          {:jason, "~> 1.3"} # Optional
      ]
    end
    

    Event store

    Next, we must decide what store to use to persist our events. We have two primary options to choose from:

    • PostgreSQL-based EventStore
    • Greg Young's EventStoreDB

    Both options are great and have their pros and cons but to make things simpler I’ll suggest using PostgreSQL adapter in the beginning.

    We will add another dependency for that:

    def deps do
      [
        {:eventstore, "~> 1.4"}
      ]
    end
    

    as well as MyApp.EventStore module which we will include in our supervision tree:

    # event_store.ex
    defmodule MyApp.EventStore do
      use EventStore, otp_app: :my_app
    
      def init(config) do
        {:ok, config}
      end
    end
    
    # application.ex
    defmodule MyApp.Application do
      use Application
    
      def start(_type, _args) do
        children = [
          MyApp.EventStore
        ]
    
        opts = [strategy: :one_for_one, name: MyApp.Supervisor]
        Supervisor.start_link(children, opts)
      end
    end
    

    Finally, a bunch of configurations to set up our store:

    config :my_app, MyApp.EventStore,
      serializer: EventStore.JsonSerializer,
      username: "postgres",
      password: "postgres",
      database: "eventstore",
      hostname: "localhost"
    
    config :my_app, event_stores: [MyApp.EventStore]
    

    And now we are good to go and run mix tasks for our event store initialization:

    mix do event_store.create, event_store.init

    By Your Command! - Commanded library quickstart

    Elixir commanded library quickstart

    Commands & Events

    In the simplest words, commands represent intentions to perform actions, while events represent the outcomes of those actions.

    A command should include a field that uniquely identifies the aggregate instance, such as "competition_id." Because commands are standard Elixir structs, you can improve the process of defining structs, enforcing mandatory keys, or performing validations by employing libraries like typed_struct or domo. This approach reduces the need for excessive boilerplate code.

    defmodule MyApp.Commands.CreateCompetition do
      @moduledoc """
      Create a new competition command
      """
    
      use Domo
    
      @type t() :: %__MODULE__{
              competition_id: nil | binary(),
              name: binary(),
              url: url(),
              user_id: binary()
            }
    
      defstruct [
        :competition_id,
        :name,
        :url,
        :user_id
      ]
    end
    

    Domain events signify significant occurrences within the scope of an aggregate. They are conventionally named in the past tense, such as "competition created".

    For each domain event, create a separate module and define its fields using defstruct. Ensure that each event also includes a field to uniquely identify the aggregate instance, such as “competition_id”

    Additionally, remember to implement the Jason.Encoder protocol for the event struct to enable JSON serialization, as illustrated below.

    defmodule MyApp.Events.CompetitionCreated do
      @moduledoc """
      Competition created event
      """
    
      @derive [Jason.Encoder]
      @type t() :: %__MODULE__{
              competition_id: binary(),
              name: binary(),
              url: binary(),
              user_id: binary()
            }
      defstruct [
        :competition_id,
        :name,
        :url,
        :user_id
      ]
    end
    

    Aggregates

    Aggregates play a crucial role in DDD by defining consistency boundaries within a domain model and encapsulating domain logic. A single aggregate is a cluster of domain objects that are treated as a single unit for the purpose of data changes. In Commanded, aggregate is comprised of its state, public command functions, command handlers functions, and state mutators. As we store events, we have access to a detailed event log of past events handled in our application.

    defmodule MyApp.Aggregates.Competition do
      @moduledoc """
      Competition write model aggregate
      """
    
      use Domo
    
      @type t() :: %__MODULE__{
              id: binary(),
              name: binary(),
              url: binary()
            }
      defstruct [
        :id,
        :name,
        :url
      ]
    
      alias MyApp.Aggregates.Competition
      alias MyApp.Commands.CreateCompetition
      alias MyApp.Events.CompetitionCreated
    
    # Public API
      def create_competition(%Competition{id: nil}, uid, name, url, user_id) do
        event = %CompetitionCreated{
                competition_id: uid,
          name: name,
          url: url,
          user_id: user_id
        }
    
        {:ok, event}
      end
    
    # Command handler
      def execute(%Competition{id: nil}, %CreateCompetition{} = command) do
        %CompetitionCreated{
                competition_id: command.id,
          name: command.name,
          url: command.url,
          user_id: command.user_id
        }
      end
    
    # State mutator
      def apply(%Competition{} = competition, %CompetitionCreated{} = event) do
        %Competition{
          competition
          | competition_id: event.id,
                    name: event.name,
            url: event.url
        }
      end
    end
    

    Dispatchers & Handlers

    At the opposite of handling command execution directly inside aggregate, we can use a separate module. A handler accepts both the aggregate and the command undergoing execution. This handler provides the opportunity to validate, authorize, and/or augment the command with supplementary data before executing the relevant function within the aggregate module.

    defmodule MyApp.Handlers.CreateCompetitionHandler do
      @behaviour Commanded.Commands.Handler
    
        alias MyApp.Commands.CreateCompetition
      alias MyApp.Aggregates.CompetitionAggregate
    
      def handle(%CompetitionAggregate{} = aggregate, %CreateCompetition{} = command) do
        %CreateCompetition{id: uid, name: name, url: url, user_id: user_id} = command
    
        CompetitionAggregate.create_competition(aggregate, uid, name, url, user_id)
      end
    end
    

    A Router module serves the purpose of dispatching commands to their designated command handler and/or aggregate module.

    To create a router module, utilize Commanded.Commands.Router, and proceed to register each command along with its corresponding handler.

    defmodule MyApp.Router do
      use Commanded.Commands.Router
    
        alias MyApp.Commands.CreateCompetition
      alias MyApp.Aggregates.CompetitionAggregate
    
      dispatch CreateCompetition, to: CompetitionAggregate, identity: :competition_id
    end
    

    Projectors & Projections

    The concept involves software components known as Projections and Projectors, which subscribe to the real-time event stream from the events database. Upon receiving an event, the Projection can then translate the data contained within that event into a view model within a designated reporting database.

    You have the flexibility to opt for an SQL or NoSQL database, a document store, a filesystem, a full-text search index, or any other storage mechanism. Moreover, you can employ multiple storage providers, tailored to optimize the querying requirements they need to fulfill.

    The simplest solution will be using the Commanded Ecto Projections library.

    This part will be our read model and if we don't change the default behaviour by ourselves it will be updated asynchronously so after the write model finishes saving events to the event store we can't be sure read models are also updated.

    defmodule MyApp.Projectors.Competition do
      use Commanded.Projections.Ecto,
        application: MyApp.Application,
        repo: MyApp.Projections.Repo,
        name: "MyApp.Projectors.Competition"
    
        alias MyApp.Events.CompetitionCreated
        alias MyApp.Projections.Competition
    
      project %CompetitionCreated{} = event, _metadata, fn multi ->
        %CompetitionCreated{name: name, url: url, user_id: user_id} = event
    
        projection = %Competition{name: name, url: url, user_id: user_id}
    
        Ecto.Multi.insert(multi, :competition, projection)
      end
    end

    FAQ

    What is Command Query Responsibility Segregation (CQRS) in Elixir?

    CQRS is a design pattern that separates the responsibilities of command execution and data querying in applications. In Elixir, this means using different models for updating state (commands) and for reading data (queries), enhancing performance and scalability.

    How does Event Sourcing complement CQRS in Elixir applications?

    Event Sourcing involves storing changes to application state as a sequence of events. This approach, combined with CQRS, enables powerful features like audit trails, time travel, and the simplification of complex business transactions in Elixir applications.

    What are the benefits of using Event Sourcing in Elixir?

    Event Sourcing offers several advantages, including robust audit logs, the ability to replay events for debugging or analysis (time travel), simplified root cause analysis, and support for event-driven architectures which can improve application responsiveness and scalability.

    What challenges does Eventual Consistency introduce in Elixir systems?

    Eventual consistency refers to the delay between executing a command and having the updated state available for queries. This delay can affect system design and user experience, requiring strategies to handle the temporary inconsistency between command execution and data availability.

    How does the Commanded library facilitate CQRS and Event Sourcing in Elixir?

    The Commanded library provides Elixir developers with tools to implement CQRS and Event Sourcing patterns efficiently. It helps in organizing commands, events, and aggregates, facilitating the development of event-driven, scalable applications.

    How do you set up an event store in Elixir using Commanded?

    Setting up an event store in Elixir involves choosing a storage backend (e.g., PostgreSQL), adding the necessary dependencies, configuring the event store module, and initializing it to persist events.

    What role do Aggregates play in an Elixir CQRS/Event Sourcing system?

    Aggregates define consistency boundaries and encapsulate business logic in CQRS/Event Sourcing systems. They manage state changes by processing commands and applying events to maintain the integrity of transactional operations.

    How are commands and events defined and processed in Commanded-based Elixir applications?

    Commands in Commanded represent actions to change the system state, while events record these changes. Developers define commands and events using Elixir structs, handling them with aggregates or processors to implement business logic and state transitions.

    What is the purpose of Projectors and Projections in Elixir's CQRS/Event Sourcing systems?

    Projectors and Projections manage the read model in CQRS/Event Sourcing systems. They listen to events and update view models or databases to reflect the current application state, supporting efficient data queries.

    Curiosum Elixir and React Developer Krzysztof
    Krzysztof Janiec Elixir & React Developer

    Read more
    on #curiosum blog