Command bus in a Rails application

… and check why 5600+ Rails engineers read also this

Command bus in a Rails application

Using commands is an important part of a DDD/CQRS-influenced architecture. In this blogpost I’d like to show you how to use the Arkency Command Bus gem within a Rails application.

Let’s first look at what a command may look like:


class AddCostCode < Dry::Types::Struct
  attribute :code, Types::String
  attribute :description, Types::String
  attribute :company_id, Types::String
end

As you see, it’s just a data structure. We’ve used DryTypes here, but you can use whatever you want, as long as it may help you define the expected params and have some basic “validations”.

Now, let’s look how it’s used from a Rails controller:


class CostCodesController < ApplicationController
  def create
    execute(AddCostCode.new(cost_code_params))
    head :created
  rescue Dry::Types::StructError => err
    head :unprocessable_entity
    raise err
  end
end

The execute method is defined in the ApplicationController as it is used from many controllers:


class ApplicationController < ActionController::Base

  def execute(command)
    command_executor.execute(command)
  end

  def command_executor
    @command_executor ||= CommandExecutor.new
  end
end

So, there’s a CommandExecutor class which is responsible for dispatching commands.


class CommandExecutor
  include EventStoreConfiguration

  def initialize
    @bus = Arkency::CommandBus.new

    register_commands
  end

  def execute(command)
    @bus.(command)
    resubscribe_processes(event_store)
  end

  private
  def register_commands
    @bus.register(AddCostCode, AddCostCodeHandler.new(repository))
    # ...
  end

  def repository
    AggregateRoot::Repository.new(event_store)
  end
end

In this case, we declare a dedidacted command handler, called AddCostCodeHandler.

class AddCostCodeHandler < CommandHandler
  attr_reader :repository

  def initialize(repository)
    @repository = repository
  end

  def call(command)
    aggregate(CompanyCostCentre, Company.new(id: command.company_id)) do |company_settings|
      company_settings.add_cost_code(command.code, command.description)
    end
  end
end

What is the CommandHandler class which we inherit from?

class CommandHandler
  protected
  def aggregate(aggregate_type, *aggregate_id, &block)
    if block
      load(aggregate_type, *aggregate_id).tap do |aggregate|
        block.call(aggregate)
        publish_changes(aggregate)
      end
    else
      load(aggregate_type, aggregate_id)
    end
  end

  private
  def load(aggregate_type, *aggregate_id)
    aggregate_type.new(*aggregate_id).tap do |aggregate|
      repository.load(aggregate)
    end
  end

  def publish_changes(aggregate)
    repository.store(aggregate)
  end
end

We use a full-CQRS approach here together with event sourcing and aggregates. Let’s look at the aggregate here:

class CompanyCostCentre
  include AggregateRoot::Base

  def initialize(company)
    @company = company
    @codes = []
  end

  def add_cost_code(code, description)
    ensure_code_is_unique(code)
    apply(cost_code_added(code, description))
  end

  def cost_code_added(code, description)
    CostCodeAdded.new(data: {
        code: code,
        description: description,
        company_id: company.id
    })
  end

This means, that we’re publishing a successful CostCodeAdded event, which can be used in other places of the system. One main place may be a read model - to help us retrieve the data from the system. (In CQRS read models serve as the Query part)

How are the events then connected?

module EventStoreConfiguration
  def event_store
    @client ||= RailsEventStore::Client.new.tap do |client|
      client.subscribe(BuildCostCodeReadModel.new, [CostCodeAdded])
    end
  end
end
class BuildCostCodeReadModel

  def call(event)
    case event.class.to_s
      when 'CostCodeAdded' then handle_cost_code_added(event)
    end
  end

  def handle_cost_code_added(event)
    CompanyCostCode.create!(code: event.data.code, company_id: event.data.company_id, description: event.data.description)
  end
end

In which, the CompanyCostCode is just a normal ActiveRecord class:

class CompanyCostCode < ActiveRecord::Base
  def self.all_for_company(company)
    where(company_id: company.id)
  end
end

And on the read side of the application, it’s used from a controller, as any other ActiveRecord objects collection.

PS

Check out more patterns that can help you in maintaining Rails apps in our Fearless Refactoring: Rails controllers ebook

You might also like