Ruby Event Store - use without Rails

Ruby Event Store v0.27 is here with some nice improvements. Let’s have a quick look.

Using RES without Rails

We’ve always built our ecosystem of gems with the intention of not being coupled to Rails. So the majority of features are implemented in ruby_event_store gem and a few other features such as async handlers integrated with ActiveJob are in rails_event_store. Every dependency is taken in a constructor and can be swapped to something different.

Until now we had one coupling which prevented you from using ruby_event_store easily. The rails_event_store_active_record gem (which provides an implementation for a repository to save events) depended on rails because it provided a migration generator to create necessary tables for storing events.

rails_event_store_active_record now integrates with rails optionally and can work without it. So you can use ruby_event_store together with rails_event_store_active_record without rails. Here is how:

Add ruby_event_store too your Gemfile:

source 'https://rubygems.org'

gem 'activerecord'
gem 'ruby_event_store'
gem 'rails_event_store_active_record'

# And one of:
gem 'sqlite3'
gem 'pg'
gem 'mysql2'

As you are not using rails and its generators, please create required database tables which are equivalent to what our migration would do in whatever way you manage DB schema in your project.

You can now use RES in your app.

require 'active_record'
require 'rails_event_store_active_record'
require 'ruby_event_store'

ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.establish_connection(ENV['DATABASE_URL'])

class OrderPlaced < RubyEventStore::Event
end

event_store = RubyEventStore::Client.new(
  repository: RailsEventStoreActiveRecord::EventRepository.new
)

event_store.publish_event(OrderPlaced.new(data: {
    order_id: 1,
    customer_id: 47271,
    amount: BigDecimal.new("20.00"),
  }),
  stream_name: "Order-1",
)

Protobuf & Custom event mappers

Since the beginning events had to inherit from RubyEventStore::Event. That is no longer the case, however. Now, any object can be used as events, provided you tell us how to map it to columns that we store in the database. To do that you can implement a custom mapper.

This new ruby_event_store version comes with RubyEventStore::Mappers::Protobuf mapper which you can use to store messages generated with google-protobuf gem.

Add RES and protobuf to your app’s Gemfile:

gem 'google-protobuf'
gem 'rails_event_store' # or `ruby_event_store` 

Configure protobuf mapper:

Rails.application.configure do
  config.to_prepare do
    Rails.configuration.event_store = RailsEventStore::Client.new(
      repository: RailsEventStoreActiveRecord::EventRepository.new(
        mapper: RubyEventStore::Mappers::Protobuf.new
      )
    )
  end
end

Define your events in protobuf file format i.e.: events.proto3

syntax = "proto3";
package my_app;

message OrderPlaced {
  string event_id = 1;
  string order_id = 2;
  int32 customer_id = 3;
}

and generate the Ruby classes:

# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: events.proto3

require 'google/protobuf'

Google::Protobuf::DescriptorPool.generated_pool.build do
  add_message "my_app.OrderPlaced" do
    optional :event_id, :string, 1
    optional :order_id, :string, 2
    optional :customer_id, :int32, 3
  end
end

module MyApp
  OrderPlaced = Google::Protobuf::DescriptorPool.
                  generated_pool.
                  lookup("my_app.OrderPlaced").
                  msgclass
end

You can now use those structures when publishing events with Rails/Ruby Event Store

event_store = Rails.configuration.event_store

event = MyApp::OrderPlaced.new(
  event_id: "f90b8848-e478-47fe-9b4a-9f2a1d53622b",
  customer_id: 123,
  order_id: "K3THNX9",
)
event_store.publish_event(event, stream_name: "Order-K3THNX9")

The work is not yet finished. We are still working on enabling this feature for async handlers but we think this a good start.

We believe this will make much easier to use RES and exchange events between multiple applications and/or micro-services.

You can now serialize your events however you want: protbuf, messagepack, Apache Avro, BSON, Thrift, Cap’n Proto. It’s up to you. You just need to implement a custom mapper with 3 methods.

Here is an example of how you could serialize your events with messagepack.

Implement the mapper:

require 'msgpack'

class MyHashToMessagePackMapper
  def event_to_serialized_record(domain_event)
    # Use data (and metadata if applicable) fields
    # to store serialized representation
    # of your domain event 
    SerializedRecord.new(
      event_id:   domain_event.fetch('event_id'),
      metadata:   "",
      data:       domain_event.to_msg_pack,
      event_type: domain_event.fetch('event_type')
    )
  end

  # Deserialize proper object based on
  # event_type and data+metadata fields
  def serialized_record_to_event(record)
    MessagePack.unpack(record.data)
  end

  def add_metadata(event, key, value)
    event[key.to_s] = value
  end
end

Pass it as a dependency:

Rails.application.configure do
  config.to_prepare do
    Rails.configuration.event_store = RailsEventStore::Client.new(
      repository: RailsEventStoreActiveRecord::EventRepository.new(
        mapper: MyHashToMessagePackMapper.new
      )
    )
  end
end

and now you can publish an event:

Rails.configuration.event_store.publish_event({
  'event_id'     => SecureRandom.uuid,
  'order_id'     => 'K3THNX9',
  'event_type'   => 'OrderPlaced',
  'order_amount' => BigDecimal.new('120.55'),
}, stream_name: 'Order$K3THNX9')

Documentation

Read more

If you enjoyed that story, subscribe to our newsletter. We share our everyday struggles and solutions for building maintainable Rails apps which don’t surprise you.

Also worth reading:

Support us by buying our latest book Domain-Driven Rails. Especially recommended for reading if you work with big, complex Rails apps.