Skip to content

fhwang/event_sourced_record

Repository files navigation

Event Sourced Record

Event Sourced Record offers an idiomatic way to use the Event Sourcing pattern in Rails code.

With Event Sourcing, every change to the state of an object is recorded as an immutable event in a replayable sequence. The result is decoupled code that simplifies state debugging and retrospective reporting.

For more, see Martin Fowler's writeup of the pattern: http://martinfowler.com/eaaDev/EventSourcing.html

Requirements

Event Sourced Record supports Rails 3.2 and higher.

Installation

Add this line to your application's Gemfile:

gem 'event_sourced_record'

And then execute:

$ bundle

Or install it yourself as:

$ gem install event_sourced_record

Event Sourced Record uses observers, so you'll need to add them to your Gemfile:

gem 'rails-observers'

Usage

See Getting_Started.md for a detailed example.

Generate the required classes with rails generate event_sourced_record:

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

The argument list is the same as with rails generate model. This will generate the event model, the projection, the calculator, the observer, and a Rake file.

Event model

The event model is an ActiveRecord model but it can act significantly different based on what type of event it is. Use event_type to configure these types:

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

  event_type :change_settings do
    attributes :bottles_per_shipment

    validates :bottles_per_shipment, numericality: true
  end
end

If you are using mass-assignment protection, which is on by default in Rails 3.2, you may want to make these attributes mass-assignable with attr_accessible.

The easiest way to create these records is with the scopes that are automatically generated by event_type:

SubscriptionEvent.creation.create!(
  bottles_per_shipment: 1, bottles_purchased: 6, user_id: current_user.id
)

The event attributes are stored in the data column, which by default is serialized to a text field. If you'd prefer to use e.g. JSONB with PostgreSQL 9.4+, just remove the call to serialize and modify the migration to use your preferred type.

Projection

The projection is the ActiveRecord model that is generated deterministically with the data in the timestamped events combined with the logic in the calculator. Projections shouldn't have any code for modifying themselves, as that will be done externally. Accordingly, projections end up being fairly small classes:

class Subscription < ActiveRecord::Base
  has_many :events, 
    class_name: 'SubscriptionEvent', 
    foreign_key: 'subscription_uuid', 
    primary_key: 'uuid'

  validates :uuid, uniqueness: true
end

(uuid is core to Event Sourced Model, so please don't remove its validations.)

Calculator

The calculator is a service class that idempotently calculates the state of the projection by running one method for each event, in order. Name the methods advance_[event_type].

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

  def advance_change_settings(event)
    @subscription.bottles_per_shipment = event.bottles_per_shipment
  end
end

Calculators can also include other associated models, which can come in handy (as long as those models don't change significantly after creation). Add that class to events and name the advance method advance_[underscored class name]:

class SubscriptionCalculator < EventSourcedRecord::Calculator
  events :subscription_events, :shipments

  def advance_shipment(shipment)
    @subscription.bottles_left -= shipment.num_bottles
  end
end

Observer

The observer simply tracks creations of events related to the projection and runs the calculator every time. You may never have to modify what the generator creates for you:

class SubscriptionEventObserver < ActiveRecord::Observer
  observe :subscription_event

  def after_create(event)
    SubscriptionCalculator.new(event).run.save!
  end
end

If you use other models as events, simply add them to the observe method:

class SubscriptionEventObserver < ActiveRecord::Observer
  observe :subscription_event, :shipment

Rake file

The Rake file gives you a convenient command to rebuild every projection whenever necessary. Since you'll be building the calculator to be idempotent, this will be a fairly safe operation.

namespace :subscription do
  task :recalculate => :environment do
    Subscription.all.each do |subscription| 
      SubscriptionCalculator.new(subscription).run.save!
    end
  end
end

Contributing

  1. Fork it ( https://github.com/[my-github-username]/event_sourced_record/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

About

Event Sourcing with Active Record.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •  

Languages