RailsEventStore 2.19: Starting Gun for 3.0

… and check why 5600+ Rails engineers read also this

RailsEventStore 2.19: Starting Gun for 3.0

RailsEventStore 2.19.1 is out — grab that one, not 2.19.0 (more on why below).

This release is the starting gun for 3.0. We’ve added deprecation warnings for everything we’re removing in the next major version. Run your test suite — every warning you see is a hard error in 3.0.

Deprecations

We’re deprecating a batch of APIs in 2.19 that will be removed in 3.0.

RubyEventStore

in_batches_of

Renamed to in_batches for consistency with the rest of the API.

# deprecated
event_store.read.in_batches_of(100).each { |batch| ... }

# use instead
event_store.read.in_batches(100).each { |batch| ... }

of_types

Renamed to of_type. Singular, consistent with other query methods.

# deprecated
event_store.read.of_types([OrderPlaced, OrderShipped])

# use instead
event_store.read.of_type([OrderPlaced, OrderShipped])

Projection API

The old API coupled projection definition to the data source upfront — you had to specify the stream when building the projection. The new API separates these concerns: define the projection once, call it with any scope from event_store.read.

# deprecated
Projection
  .from_stream("Order$1")
  .init(-> { 0 })
  .when(OrderPlaced, ->(state, event) { state + 1 })
  .run(event_store)

# use instead
Projection
  .init(-> { 0 })
  .on(OrderPlaced, ->(state, event) { state + 1 })
  .call(event_store.read.stream("Order$1"))

Also deprecated: calling Projection#call with multiple scopes — pass a single scope. Use Projection.init instead of Projection.new.

Class-based subscribers

Passing a class as a subscriber hides the lifecycle — RES had to decide when and how to instantiate it, with no control from the caller. An instance or lambda is explicit about what gets called and when.

# deprecated
event_store.subscribe(SendOrderConfirmation, to: [OrderPlaced])

# use instead
event_store.subscribe(SendOrderConfirmation.new, to: [OrderPlaced])

EventClassRemapper / events_class_remapping:

Upcasting has been available since RES 2.1.0 and is the proper way to handle renamed or evolved event classes. It’s composable, co-located with the transformation logic, and doesn’t require configuring the mapper globally. EventClassRemapper is being removed.

NullMapper

Mappers::Default.new without arguments does exactly what NullMapper did, with a name that doesn’t imply it does nothing.

# deprecated
mapper: RubyEventStore::Mappers::NullMapper.new

# use instead
mapper: RubyEventStore::Mappers::Default.new

RailsEventStore

RailsEventStore::* constant aliases

The rails_event_store gem is an integration layer — it wires RES into Rails. The domain objects (events, client, projections) live in ruby_event_store. Aliasing them under the RailsEventStore namespace implied they were Rails-specific, which caused confusion about what’s portable and what isn’t. In 3.0 those aliases are gone — use the source namespace directly.

# deprecated
RailsEventStore::Event
RailsEventStore::JSONClient

# use instead
RubyEventStore::Event
RubyEventStore::JSONClient

*.rails_event_store instrumentation events

Same reason as above — the implementation is in ruby_event_store, so the instrumentation namespace should be too. During the 2.19 transition period both *.rails_event_store and *.ruby_event_store are dual-fired. After 3.0 only *.ruby_event_store remains — update your ActiveSupport::Notifications subscriptions.

Dispatcher naming

The Async in ImmediateAsyncDispatcher and AfterCommitAsyncDispatcher described the handler (a background job), not the dispatcher itself. The new names drop the misleading qualifier. Dispatcher becomes SyncScheduler — a more accurate description of what it actually does.

Deprecated Use instead
ImmediateAsyncDispatcher ImmediateDispatcher
AfterCommitAsyncDispatcher AfterCommitDispatcher
Dispatcher SyncScheduler

AggregateRoot

apply_* method convention

The old convention mapped event handlers by method name — apply_order_placed would handle OrderPlaced. The problem, which we even documented at the time: you can’t grep for usages of the event class. The on DSL references the event class explicitly.

# deprecated
class Order
  include AggregateRoot

  def apply_order_placed(event)
    @status = :placed
  end
end

# use instead
class Order
  include AggregateRoot

  on OrderPlaced do |event|
    @status = :placed
  end
end

AggregateRoot::Configuration / default_event_store

Global state with a hidden dependency on the event store. Makes testing harder, makes the dependency invisible at the call site. Pass the event store explicitly to Repository.new.

# deprecated
AggregateRoot::Configuration.new.tap do |c|
  c.default_event_store = Rails.configuration.event_store
end

# use instead
repository = AggregateRoot::Repository.new(Rails.configuration.event_store)

PostgreSQL valid_at index

If you use bi-temporal queries (as_of), add this index.

PostgreSQL can’t use a regular column index for an expression in ORDER BY — it needs a dedicated functional index. Without it, as_of queries fall back to a sequential scan. On a table with ~100k events that’s ~6 seconds. On tables with millions of events, even small result sets take 800ms–1600ms. The mechanics are covered in detail in How to add index to a big table of your Rails app.

New installations get a functional index on COALESCE(valid_at, created_at) automatically. Existing installations:

bin/rails generate rails_event_store_active_record:migration_for_valid_at_index
bin/rails db:migrate

The generator in 2.19.0 used a plain CREATE INDEX — which locks the table for the duration of the build. We caught it and shipped 2.19.1 the next day with algorithm: :concurrently and disable_ddl_transaction!.

PostgreSQL only — MySQL and SQLite use different syntax for expression indexes.

Under the hood

The CI matrix now covers Ruby 4.0, Rails 8.1, Redis 8, PostgreSQL 18, and MySQL 9.7. We’ve dropped EOL versions: Ruby 3.2 (EOL March 2026), old Rails and ActiveRecord versions, PostgreSQL 13, MySQL 8.0.

The test suite previously used multiple per-version dummy Rails apps. We’ve consolidated these into a single app driven by different Gemfiles across the CI matrix.

Mutation coverage gaps have been closed — some after the tag cut.

Upgrading

gem 'rails_event_store', '~> 2.19'

Full release notes: v2.19.0, v2.19.1

You might also like