Rails Event Store

Event serialization formats

By default RailsEventStore will use YAML as a serialization format. The reason is that YAML is available out of box and can serialize and deserialize data types which are not easily handled in other formats.

However, if you don't like YAML or you have different needs you can choose to use different serializers or even replace mappers entirely.

Configuring a different serializer

You can pass a different serializer as a dependency when instantiating the client.

Here is an example on how to configure RailsEventStore to serialize events' data and metadata using Marshal.

# config/environments/*.rb

Rails.application.configure do
  config.to_prepare do
    Rails.configuration.event_store = RailsEventStore::Client.new(
      mapper: RubyEventStore::Mappers::Default.new(
        serializer: Marshal
      )
    )
  end
end

The provided serializer must respond to load and dump.

Configuring a different mapper

JSON is a popular serialization choice when paired with JSON/JSONB storage on Potgres. To make full use of it, set:

# config/environments/*.rb

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

Additionally, if you are using Postgres, you can tell the migration to use JSON or JSONB data types for the data and metadata fields by passing in a --data-type to the generator.

$ rails generate rails_event_store_active_record:migration --data-type=jsonb

Available mappers

  • RubyEventStore::Mappers::Default
    • works with RubyEventStore::Event or RailsEventStore::Event
    • constructor takes named arguments:
    • serializer: (described above)
    • events_class_remapping: - which can be used for mapping old event names to new ones after you refactored your codebase.
  • RubyEventStore::Mappers::Protobuf
    • works with Ruby classes generated by google-protobuf gem
  • RubyEventStore::Mappers::NullMapper
    • works with RubyEventStore::Event or RailsEventStore::Event
    • no transformations, useful in tests
  • RubyEventStore::Mappers::EncryptionMapper
    • works with RubyEventStore::Event or RailsEventStore::Event
    • constructor takes named arguments:
    • key_repository - which is responsible for providing encryption keys used to encrypt/descrypt payload by encryption transformation
    • serializer: (described above)

Custom mapper

Mapper is defined as a pipeline of transformations that transforms domain event object into serialized record and back from serialized recoed to domain event. I.e. DefaultMapper is implemented as a set of 3 transformations. Transformations works on transformation item objects, it is given to transformation methods (it is expected that load & dump methods are implemented) and it is result of any transformation performed. Except transformations each pipeline mapper needs to be given 2 edge transformations:

Extended implementation of DefaultMapper with explicit default arguments:

module RubyEventStore
  module Mappers
    class Default < PipelineMapper
      def initialize(serializer: RubyEventStore::Serializers::YAML, events_class_remapping: {})
        super(Pipeline.new(
          to_domain_event: Transformation::DomainEvent.new,
          to_serialized_record: Transformation::Record.new,
          transformations: [
            Transformation::EventClassRemapper.new(events_class_remapping),
            Transformation::SymbolizeMetadataKeys.new,
            Transformation::Serialization.new(serializer: serializer),
          ]
        ))
      end
    end
  end
end

When you define new custom mapper you could use mapper PipelineMapper as base class and provide your transformations pipeline.

Each transformation must implement 2 methods: dump(item) & load(item). Both methods take a RubyEventStore::Mappers::Transformation::Item and returns new instance of it, transformed. Transformation shall not modify given transformation item and shall return new instance as a result.

If you use different classes to represent domain event & serialized record you shall also define edge transformations for your pipeline definition.

Example of custom mapper build based on mappers pipeline:

require 'msgpack'

class MessagePackSerialization
  def dump(item)
    RubyEventStore::Mappers::Transformation::Item.new(
      event_id:   item.event_id,
      metadata:   item.metadata.to_msg_pack,
      data:       item.data.to_msg_pack,
      event_type: item.event_type
    )
  end

  def load(item)
    RubyEventStore::Mappers::Transformation::Item.new(
      event_id:   item.event_id,
      metadata:   MessagePack.unpack(item.metadata),
      data:       MessagePack.unpack(item.data),
      event_type: item.event_type
    )
  end
end

class MyHashToMessagePackMapper < RubyEventStore::Mappers::PipelineMapper
  def initialize
    super(RubyEventStore::Mappers::Pipeline.new(
      transformations: [
        MessagePackSerialization.new,
      ]
    ))
  end
end

You could build your own transformations or use existing ones combined with the ones build by you. We strongly encourage you to share your transformations as a part of RES-contrib.

Check out the code of our mappers and transformations on github for examples on how to implement mappers & transformations.

You can pass a different mapper as a dependency when instantiating the client.

# config/environments/*.rb

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

Now you should be able to publish your events:

class OrderPlaced < RubyEventStore::Event
end

event_store = Rails.configuration.event_store

event_store.publish(OrderPlaced.new(data: {
  'event_id' => SecureRandom.uuid,
  'order_id' => 1,
  'order_amount' => BigDecimal.new('120.55'),
}), stream_name: 'Order$1')