What's New in Rails Event Store 3.0
… and check why 5600+ Rails engineers read also this
What’s New in Rails Event Store 3.0
Rails Event Store 3.0 is primarily a cleanup release.
Throughout the 2.x series we introduced new features while gradually deprecating APIs we no longer wanted to maintain. In 3.0, those deprecated APIs are finally gone.
There are no major new concepts or APIs to learn. The public API is simply smaller, more consistent, and comes with a few stricter defaults.
If you’ve already addressed all deprecation warnings in 2.19, upgrading to 3.0 should be straightforward. This post summarizes what changed, while the 2.19 release announcement explains the motivation behind each deprecation in more detail.
Lean API: the deprecations are gone
The 2.x series was conservative — we kept old method names alive and just warned you about them. 3.0 removes the training wheels: every name kept around for compatibility is gone. Here’s the at-a-glance list of what’s removed and what replaces it (each row links to the reasoning in the 2.19 post):
| Removed | Use instead |
|---|---|
read.in_batches_of(100) |
read.in_batches(100) details |
read.of_types([Type]) |
read.of_type(Type) details |
RubyEventStore::ImmediateAsyncDispatcher |
RubyEventStore::ImmediateDispatcher details |
RailsEventStore::AfterCommitAsyncDispatcher |
RailsEventStore::AfterCommitDispatcher details |
RubyEventStore::Dispatcher |
RubyEventStore::SyncScheduler details |
subscribe(Handler, to: [Type]) |
subscribe(Handler.new, to: [Type]) details |
Mappers::NullMapper.new |
Mappers::Default.new details |
def apply_order_placed(event) |
on(OrderPlaced) { ... } details |
AggregateRoot::Configuration |
AggregateRoot::Repository.new(event_store) details |
read.repository.rails_event_store |
read.repository.ruby_event_store details |
Projection |
Projection details |
RailsEventStore::Event |
RubyEventStore::Event details |
Note — removing the aliases doesn’t touch the genuinely Rails-specific classes.
RailsEventStore::ClientandRailsEventStore::AfterCommitDispatcheraren’t re-exports of anything — they exist only because of Rails (ActiveRecord, transaction callbacks), so they stay. Only the constants that merely pointed at aRubyEventStore::original are gone.
The one replacement that isn’t a rename is EventClassRemapper — its successor, upcasting, needs a real handler.
EventClassRemapper is gone — use upcasting
In event sourcing, events are immutable facts — once written, a record’s event_type stays as it was, a plain string you never go back and rewrite. By convention that string is the class name, and that’s exactly what a rename breaks: move OrderPlaced into an Ordering module and the events already stored as "OrderPlaced" no longer resolve to Ordering::OrderPlaced on read.
In 2.x you patched this on read with the events_class_remapping: option — a string-to-string lookup:
# 2.x — removed
RubyEventStore::Mappers::Default.new(
events_class_remapping: { "OrderPlaced" => "Ordering::OrderPlaced" }
)
3.0 replaces it with the upcasting transformation. A Record is immutable, so the upcast lambda receives the old record and returns a brand-new one — for a rename you change only event_type:
# 3.0
upcast = RubyEventStore::Mappers::Transformation::Upcast.new(
"OrderPlaced" => ->(record) do
RubyEventStore::Record.new(
event_type: "Ordering::OrderPlaced",
data: record.data,
event_id: record.event_id,
metadata: record.metadata,
timestamp: record.timestamp,
valid_at: record.valid_at,
)
end
)
mapper = RubyEventStore::Mappers::Pipeline.new(
upcast,
RubyEventStore::Mappers::Transformation::SymbolizeMetadataKeys.new,
)
RailsEventStore::Client.new(mapper: mapper)
Starting from a full Record is the whole point: it lets you do far more than rename the type. You can reshape data between versions, split or merge fields, backfill a value that older events never carried — and chain entries so each record is upgraded step by step until it stops changing. That flexibility is what the few extra lines buy you over a one-line hash.
Other removals
A handful of smaller cleanups round out the release, grouped by where they’d reach you — chances are most won’t.
If you subscribe to instrumentation
serialize/deserializemapper events removed.
They were renamed toevent_to_record/record_to_event(payload keydomain_event:→event:); update anyActiveSupport::Notificationssubscriptions on the old names.events:/messages:payload keys removed.
Theappend_to_streamandupdate_messagesnotifications now carry onlyrecords:— read that key instead.
If you customize mappers or aggregates
JSONMapperremoved.
It was a thinDefaultsubclass —Defaultalready handles JSON, so use that instead.with_default_apply_strategy/with_strategyremoved.
The default strategy already comes withinclude AggregateRoot; for a custom one, useAggregateRoot.with(strategy: …).
Stricter by default
niltopublish/append/linkis now rejected.
It used to warn and carry on — now it raisesArgumentError, so guard calls that might pass an empty result.ensure_supported_any_usageremoved.InMemoryRepositorynow always rejects mixingexpected_version: :anywith specific positions — matching the SQL repositories, so in-memory tests catch what production would.
And one warning simply went away — the spurious rails_event_store_active_record rename warning is gone (backstory in the 2.19 post).
Upgrade guide
Start on 2.19 and clear every deprecation warning first — once your test suite is quiet, the table above is your checklist and the rest of the upgrade is mostly find-and-replace. Three changes need a real edit rather than a rename.
Projection API — define the projection once, then call it with any scope:
# before
Projection
.from_stream("Orders")
.when(OrderPlaced, handler)
.run(event_store)
# after
Projection
.init({ count: 0 })
.on(OrderPlaced, &handler)
.call(event_store.read.stream("Orders"))
AggregateRoot default_event_store — the global default is gone; wire the event store explicitly through AggregateRoot::Repository.new(event_store). Details in the 2.19 post.
EventClassRemapper — replace the events_class_remapping: hash with an upcasting transformation on your mapper, as shown in the upcasting section above.