This guide gives a step by step overview on how to add Sequent to an existing Rails application.

We assume you’re already familiar with Ruby on Rails and the core Concepts of Sequent.

Prerequisites

PostgreSQL database. Sequent only supports Postgres databases. There is no particular reason for this other than that we haven’t had the need or time to support any other databases.

Guide

For a seamless integration with the latest Rails, it’s best is to adhere to the Rails naming conventions. In Rails everything under the app directory is autoloaded. To make use of this feature, it’s best is to put your domain classes under an app subdirectory. For instance in app/domain/bank_account/bank_account_aggregate.rb. In this case Rails expects your domain class to be called BankAccount::BankAccountAggregate. See the Rails autoloading and reloading guide for more details.

Installation

Add to your Gemfile

gem 'sequent'

and run bundle install

Configuration

Sequent configuration

Add Sequent configuration in config/initializers/sequent.rb with:

require_relative '../../db/sequent_migrations'

Rails.application.reloader.to_prepare do
  Sequent.configure do |config|
    config.migrations_class_name = 'SequentMigrations'
    config.enable_autoregistration = true
    config.event_store_cache_event_types = !Rails.env.development?

    config.database_config_directory = 'config'

    # this is the location of your sql files for your view_schema
    config.migration_sql_files_directory = 'db/sequent'
  end
end

We wrap the sequent initializer code inside Rails.application.reloader.to_prepare because during initialization the autoloading hasn’t run yet.

Eager loading

Enable eager loading for every Rails environment in config/environments/*.rb

config.eager_load = true

Sequent internally relies on registries of classes of certain types. For instance it keeps track of all AggregateRoot classes by adding them to a registry when Sequent::Core::AggregateRoot is extended. For this to work properly, all classes must be eager loaded, otherwise code depending on this fact might produce unpredictable results.

Sequent’s Unit Of Work

If you load Aggregates inside Controllers or ActiveJob (for example) you have to clear Sequent’s Unit Of Work (stored in the Thread.current). With Rails this can be automatically done using Rack middleware. Add this to application.rb:

config.middleware.use Sequent::Util::Web::ClearCache

This step is only necessary if you load Aggregates outside the scope of the Unit Of Work, which is automatically started and committed via the execute_commands call. See using the AggregateRepository outside the Unit Of Work for more details.

Rake tasks

Add the following snippet to your Rakefile:

# Sequent requires a `SEQUENT_ENV` environment to be set
# next to a `RAILS_ENV`
ENV['SEQUENT_ENV'] = ENV['RAILS_ENV'] ||= 'development'

require 'sequent/rake/migration_tasks'

Sequent::Rake::MigrationTasks.new.register_tasks!

# The dependency of sequent:init on :environment ensures the Rails app is loaded
# when running the sequent migrations. This is needed otherwise
# the sequent initializer - which is required to run these rake tasks -
# doesn't run
task 'sequent:init' => [:environment]
task 'sequent:migrate:init' => [:sequent_db_connect]

task 'sequent_db_connect' do
 Sequent::Support::Database.connect!(ENV['SEQUENT_ENV'])
end

# Create custom rake task setting the SEQUENT_MIGRATION_SCHEMAS for
# running the Rails migrations
task :migrate_public_schema do
 ENV['SEQUENT_MIGRATION_SCHEMAS'] = 'public'
 Rake::Task['db:migrate'].invoke
 ENV['SEQUENT_MIGRATION_SCHEMAS'] = nil
end

# Prevent rails db:migrate from being executed directly.
Rake::Task['db:migrate'].enhance([:'sequent:db:dont_use_db_migrate_directly'])

Rails db:migrate At the last line in the Rakefile we deny using the Rails’s db:migrate task directly. You can’t use this task directly anymore since that will add all the tables of the view_schema and sequent_schema to the schema.rb file after running a Rails migration. Instead, the rails db:migrate must be wrapped in your own task where you set the environment variable SEQUENT_MIGRATION_SCHEMAS. For safety reasons we’ve “enhanced” the rails db:migrate task with Sequent’s sequent:db:dont_use_db_migrate_directly task, so running it without SEQUENT_MIGRATION_SCHEMAS set will fail.

Database setup

Sequent database schema

Download the sequent/db/sequent_schema.rb file and put it in the db directory:

curl -o db/sequent_schema.rb https://raw.githubusercontent.com/zilverline/sequent/refs/heads/master/db/sequent_schema.rb

View schema migrations

Create db/sequent_migrations.rb. This will contain your view_schema migrations.

VIEW_SCHEMA_VERSION = 1

class SequentMigrations < Sequent::Migrations::Projectors
  def self.version
    VIEW_SCHEMA_VERSION
  end

  def self.versions
    {
      '1' => [
        # List of migrations for version 1
      ],
    }
  end
end

For a complete overview on how Migrations work in Sequent, check out the Migrations Guide

Database configuration

Ensure your database.yml contains the correct adapter and schema_search_path:

default: &default
   adapter: postgresql
   host: localhost
   port: 5432
   username: <%= ENV["POSTGRES_USER"] %>
   password: <%= ENV["POSTGRES_PASSWORD"] %>
   pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
   timeout: 5000
   schema_search_path: <%= ENV.fetch("SEQUENT_MIGRATION_SCHEMAS") { "public, sequent_schema, view_schema" } %>

Create the database

Run the following commands to create the sequent_schema and view_schema:

bundle exec rake sequent:db:create_event_store
bundle exec rake sequent:db:create_view_schema

# only run this when you add or change projectors in SequentMigrations
bundle exec rake sequent:migrate:online
bundle exec rake sequent:migrate:offline

Done

All done, you should be able to run the Rails application without problems

bundle exec rails server
=> Booting Puma
=> Rails 7.2.1 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 6.4.3 (ruby 3.3.5-p100) ("The Eagle of Durango")
*  Min threads: 3
*  Max threads: 3
*  Environment: development
*          PID: 54525
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop

Autoloading and reloading your domain

Rails uses Zeitwerk for autoloading and reloading. To ensure your domain classes will also benefit from this feature, put them under a subdirectory of the app folder and adhere to the Rails naming conventions.

One caveat is that this leads to an explosion of small files containing singular Event classes and Command classes. The preference of the Sequent team is to group all Event classes and Command classes in a single file (e.g. events.rb / commands.rb). Luckily in Zeitwerk this is still possible. An example folder structure:

app/
  controllers/
  models/
  domain/ # <- you can pick any name
    banking/ # <- optional subdirectory
      bank_account.rb
      events.rb
      command_handler.rb

In the example above the bank_account.rb contains the AggregateRoot and looks as follows:

module Banking # <- corresponds to the subdirectory banking
   class BankAccount < Sequent::AggregateRoot
   end
end

The events.rb contains the Event classes and looks as follows:

module Banking
   module Events # <- because our file is called `events.rb` it expects a module Events to exist.
      class BankAccountCreated < Sequent::Event; end
      class BankAccountClosed < Sequent::Event; end
   end
end

The “downside” here is that you need to introduce an extra layer of naming to be able to group your events into a single file.

Rails Engines

Sequent in Rails Engines work basically the same as a normal Rails application. Some things to remember when working with Rails Engines:

  1. The Sequent config must be set in the main application config/initializers.
  2. The main application is the maintainer of the sequent_schema and view_schema. Copy over the migration SQL files to the main application directory like you would when an Engine provides ActiveRecord migrations.

Please checkout the Rails & Sequent example app in our sequent-examples Github repository.