Introduction to Rails Event Store

Jan Dudulski02/29/2024

Introduction to Rails Event Store

Event store is a proper name for a..., well, storage of events. Events are facts from the past. Such a trait makes our storage a great candidate for append-only mode, ie. there are only two operations available: read and add. It might feel like a weird constraint but it allows to optimize mentioned operations to be as fast as possible. At the same time, simplicity creates a foundation for our creativity and making really complicated solutions.

Let's emphasize it again, in case if you missed that between the lines - we can only read the history of events and add new events. It means that we neither can modify existing events, delete them nor change the order of events in history.

When adding new events, we can also assign them to a stream. A stream can help to limit future read to just a part of the history, e.g. a stream can contain all events regarding the order.

What is Rails Event Store

Event Store is also a name for a specific database designed by Greg Young, a pioneer of CQRS and Event Sourcing. In the beginning, Rails Event Store was just a Ruby implementation of the EventStore based on ActiveRecord - trying to mimic the same API and functionality. Eventually, Rails Event Store evolved to take advantage of Ruby power and provide more support for Rails ecosystem.

Currently, Rails Event Store is actually a Ruby Event Store plus tooling, but Rails prefix sells better. Regardless, you can still use it outside the Rails project without any problems.

The Rails Event Store ecosystem consist few gems like:

  • rails_event_store - Ruby Event Store + helpers specific for Rails
  • ruby_event_store - EventStore implementation
  • ruby_event_store-rspec - matchers for RSpec
  • ruby_event_store-browser - simple UI for reading events in the web browser
  • aggregate_root - Event sourced implementation of Aggregate Root

Use cases for Rails Event Store

I won't waste time describing how to install Rails Event Store - there is not much sense to repeat the documentation (spoiler alert - it's very easy). Instead, I would like to focus on different use cases where RES might help you.

PubSub

Publish-subscribe with Rails Event Store is trivial to set up. All you need to do is to subscribe an actor to specific event types, like:

event_store.subscribe(Actor.new, to: [DomainEvents::Foo, DomainEvents::Bar])

and implement a handler for the actor:

class Actor
# will be called after publishing an event
def call(event)
end
end

Finally, you can start firing your events:

event_store.publish(
DomainEvents::Foo.new(data: {key1: "value1", key2: "value2"})
)

It might happen that you want to make a temporary subscription, just for a single call of action that should emit an event. If that's your case you can use a temporary subscription:

event_store.within do
run_the_operation
end.subscribe(to: [DomainEvents::Foo]) do |event|
render_foo
end.subscribe(to: [DomainEvents::Bar]) do |event|
render_bar
end.call # don't forget about calling a `call` here!

Audit log (and debug, and report)

Audit log comes for free if you implement event sourcing or when you introduce changes to the system via events. For a full audit log, it's worth also recording events that won't introduce a change, ie. errors, failed authentication and authorization attempts, etc. Thanks to the easy way of adding metadata you don't have to worry about missing a session id, user id, or IP of the request.

def publish_event(event, metadata = {})
event_store.with_metadata({user_id: current_user.id}.merge(metadata)) do
event_store.publish(event)
end
end

When debugging, such a log can save you a day and reduce the time needed to find a cause of a problem.

If you want to find more inspirations of how event store can help you debug problems, please watch the presentation from DDD Europe 2018 by Thomas Pierrain.

Projections

deriving a current state from a stream of events

Foldl

Projection is a foldl/reduce/inject applied to events from history. When building a projection, you can filter events by stream or event type or specific properties. As a result, you can:

  • build a highly optimized read model or cache
  • generate a report at a specific time, e.g. state of inventory at the end of a month
  • recreate a state of an entity at a time when the client reported a bug
  • rebuild a correct state of an entity after fixing a bug in a logic
  • and more!

Event Sourcing

Event sourcing is the next step of projections. Every change is applied to an entity projected from the history of events and generates another event or events.

In other words:

  • read a stream of events
  • reduce them to rebuild the current state
  • call the logic
  • save new events to the stream

Rails Event Store can help you implement such logic with AggregateRoot gem.

Process manager

The process manager is a reverse of an aggregate - aggregate receives a command and emits an event while the process manager receives events and emits a command.

Process manager can follow a process in the application that can be stretched over time or simply requires multiple actions that require a proper reaction. Classic examples are about orders (cart, confirmation, payment, shipping) or bank transactions but you can easily imagine a lot of different processes: search of flight connection, renting a car, reclamation, preparation of a party, etc.

For a Polish audience, I highly recommend the fifth episode of a Better Software Design podcast.

Async

Rails Event Store by default works in a synchronous mode which means that publishing and saving events happen in the same database transaction as calling handlers of subscribers. It's not a problem as long as not much is happening but if you ever need to improve the performance, with a few lines of code you can move the heavy part to the background by enabling asynchronous mode:

# configure event store client
event_store = RailsEventStore::Client.new(
mapper: RubyEventStore::Mappers::Default.new,
repository: RailsEventStoreActiveRecord::EventRepository.new(
serializer: RubyEventStore::NULL
),
dispatcher: RubyEventStore::ComposedDispatcher.new(
RailsEventStore::AfterCommitAsyncDispatcher.new(
scheduler: EventStoreAsyncScheduler.new
),
RubyEventStore::Dispatcher.new
)
)

# subscriber inherits from ActiveJob
class AsyncJob < ApplicationJob
def perform(payload)
event_id = payload.symbolize_keys.fetch(:event_id)
domain_event = event_store.read.event(event_id)

call(domain_event)
end
end

# use just a class as a subscriber
event_store.subscribe(AsyncJob, to: [DomainEventName])

Async has a cost of eventual consistency but allows to finish the basic request faster and to give a response to the client while leaving the heavy stuff for the future (sometimes just milliseconds later), like with sending an email from the background job instead of doing that directly in the controller action.

Browser

Rails Event Store ships with a UI that can help you in debugging but also can be used as a handy tool for showing a log of specific events to the user. It allows you to read an event, stream, and jump to the correlation and causation so you can answer a question "what happened" out of the box.

Alternatives

Rails Event Store is mainly a library for storing events and leaves a lot of decisions up to the developer. If you like more opinionated solutions that will guide you on how to build event-based architecture you can also check the Sequent project.

Jan Dudulski avatar
Jan Dudulski