fhwang.net

Event Sourcing Libraries in Ruby: A Guide

About this guide

This guide compares active, open-source Event Sourcing libraries in Ruby. It doesn't make any specific recommendations, just specific comparisons that will hopefully save programmers some time in making a decision.

Methodology

This guide considers Event Sourcing libraries in Ruby that meet a few requirements:

  • Open-source
  • Actively maintained (defined here as having commits in the last three months)
  • Do not include CQRS (more on that below)

The information was gleaned from reading the docs, the source, and provided sample apps. You'll still want to do some due diligence yourself if you want to put any of these libraries in production.

This guide will be updated over time to stay-up-to-date with the relevant libraries.

Is this guide objective?

I wrote one of the libraries below, so that choice naturally reflects many of my own biases and priorities when it comes to functionality and style. I try to discuss design trade-offs even-handedly, but you never can be sure. Keep my biases in mind.

Can I help improve this guide?

Yes, please. If you see information that is out-of-date, missing, or incorrect, please let me know and I'll try to incorporate your suggestions. I am also interested to hear of organizations that are using these libraries in production.

Why no CQRS?

CQRS, or Command Query Responsibility Segregation, is a pattern that often complements Event Sourcing. There are a number of Ruby CQRS + Event Sourcing libraries, but the inclusion of CQRS makes those libraries much larger and more ambitious. Since I have little experience with (and interest in) CQRS, I have decided to ignore libraries that include it.

If somebody writes a good CQRS Ruby library guide, I'll be happy to link to it from here.

The libraries

There are three active Event Sourcing libraries in Ruby: Event Sourced Record, Rails EventStore, and Sandthorn.

Event Sourced Record

Version reviewed Key dependencies
0.2.2 Rails >= 3.2

Event Sourced Record is my library. It's intended to help Rails apps use Event Sourcing in small ways by augmenting ActiveRecord classes.

Events are defined with blocks on an ActiveRecord event class, including attributes and validations on those attributes. Events are associated with one specific projection class via belongs_to.

class SubscriptionEvent < ActiveRecord::Base
  include EventSourcedRecord::Event

  serialize :data

  belongs_to :subscription, 
    foreign_key: 'subscription_uuid', primary_key: 'uuid'

  event_type :creation do
    attributes :bottles_per_shipment, :bottles_purchased, :user_id

    validates :bottles_per_shipment, presence: true, numericality: true
    validates :bottles_purchased, presence: true, numericality: true
    validates :user_id, presence: true
  end
end

Events are resolved with custom calculator classes.

class SubscriptionCalculator < EventSourcedRecord::Calculator
  events :subscription_events

  def advance_creation(event)
    @subscription.user_id = event.user_id
    @subscription.bottles_per_shipment = event.bottles_per_shipment
    @subscription.bottles_left = event.bottles_purchased
  end
end

The calculator is automatically fired via an observer, but you can also use it to manually replay events.

Event Sourced Record includes a generator that fills out a lot of scaffolding for you with one command.

rails generate event_sourced_record Subscription \
      user_id:integer bottles_per_shipment:integer \
      bottles_left:integer

Event Sourced Record doesn't offer a lot of functionality around tracking state within an event stream or subscribing to new events in real-time. These are ActiveRecord records, so the library leaves it up to you to manipulate them yourself if you need to.

Rails EventStore

Version reviewed Key dependencies
0.1.1 ActiveSupport >= 3.0, ActiveRecord

Rails EventStore is an event store implementation by Arkency. It focuses on event publishing and simple semantics for reading events. It uses ActiveRecord for event storage. It's not opinionated about how you should integrate Event Sourcing into the rest of a Rails app, or about how you should structure your code to process events.

Events are defined as standalone classes.

class OrderCreated < RailsEventStore::Event
end

Events are published by client instances, and upstream publishers are responsible for knowing what to put into the event. You can optionally assign a per-event stream name.

client = RailsEventStore::Client.new
stream_name = "order_1"
event_data = {
  data: { data: "sample" },
  event_id: "b2d506fd-409d-4ec7-b02f-c6d2295c7edd"
}
event = OrderCreated.new(event_data)
client.publish_event(event, stream_name)

One interesting feature is optimistic locking, which will have the client raise an error if somebody else has appended an event since the last time you looked at the stream.

begin
  expected_version = "850c347f-423a-4158-a5ce-b885396c5b73" # last event_id
  client.publish_event(event, stream_name, expected_version)
rescue RailsEventStore::AppendEventToStream
  logger.info "Appending out of order, publish failed"
end

There are a few ways to read events with the client. You can read prior events with read_all_events, optionally passing in a start event ID and a max batch size.

stream_name = "order_1"
start = "850c347f-423a-4158-a5ce-b885396c5b73"
count = 40
client.read_all_events(stream_name, start, count)

You can also process events synchronously by creating custom classes and subscribing to event types through a client.

class InvoiceReadModel
  def handle_event(event)
    # we deal here with event's data
  end
end

invoice = InvoiceReadModel.new
client.subscribe(invoice, ['PriceChanged', 'ProductAdded'])

If I'm reading the code correctly, client.subscribe is similar to Rails observers in that it only handles in-process message passing, as opposed to inter-process communication via RabbitMQ etc. If you want to have, for example, a background worker that handles events asynchronously, you'll probably write your own polling loop to do so.

Sandthorn

Version reviewed Key dependencies
0.8.0 Sequel

Sandthorn tracks events implicitly based on method names attached to projection classes. It's the only library in this guide that doesn't use Rails at all.

Events are stored in the production DB via Sequel. They are recorded by calling commit, which records an event named after the containing method (by using Kernel#caller, if you're wondering.) The event is retained in-memory on the projection and saved to the DB when the projection is saved. For example, the code below will save the renamed ship and record an event named ship_was_renamed.

class Ship
  include Sandthorn::AggregateRoot
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def rename!(new_name)
    @name = new_name
    ship_was_renamed
  end

  private

  def ship_was_renamed
    commit
  end
end

ship = Ship.new("Pequod")
ship.rename!("Titanic")
ship.save

One interesting feature is aggregate_trace. The name is a bit inscrutable IMO but it allows you attach metadata inside of a block to all events generated. For example, you might use it to attach session information to a group of events.

ship.aggregate_trace(ip_address: ip_address, user_id: user_id) do
  ship.rename!("Bounty")
  ship.set_destination!("Pitcairn")
  ship.save
end

As for how to retrieve events for re-use, this isn't really documented but it seems that you can pull them out using Sandthorn.get_events. Presumably you could use this to re-run projection calculation or re-use those events for a different purpose.

Sandthorn.get_events(aggregate_types: [Ship])

In my entirely biased opinion, I'm not sure this library qualifies as Event Sourcing, as opposed to an auditing library such as PaperTrail, because the events are generated as side-effects of changes, not as the initiator of those changes. Event definitions seem too coupled to the projection to offer many of the benefits of Event Sourcing. However, Sandthorn is being actively maintained and (presumably) has programmers who are happy to use it, so I've decided to err on the side of inclusion here.

Update history

May 10, 2015: Initial publication.

blog comments powered by Disqus