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(
      repository: RailsEventStoreActiveRecord::EventRepository.new(serializer: Marshal)
    )
  end
end

The provided serializer must respond to load and dump.

Serialization is needed not only when writing to and reading from storage, but also when scheduling events for background processing by async handlers:

Rails.configuration.event_store = RailsEventStore::Client.new(
   dispatcher: RubyEventStore::ComposedDispatcher.new(
     RailsEventStore::AfterCommitAsyncDispatcher.new(scheduler: ActiveJobScheduler.new(serializer: Marshal)),
     RubyEventStore::Dispatcher.new
   )
 )
class SomeHandler < ActiveJob::Base
  include RailsEventStore::AsyncHandler.with(serializer: Marshal)

  def perform(event)
    # ...
  end
end

Configuring a different mapper

JSON is a popular serialization choice when paired with JSON/JSONB storage on Postgres. To make full use of it set or migrate your data and metadata columns to jsonb first.

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

Next set repository serialization to RailsEventStoreActiveRecord::EventRepository.new(serializer: RubyEventStore::NULL)

# config/environments/*.rb

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

Available mappers

  • RubyEventStore::Mappers::Default
    • works with RubyEventStore::Event or RailsEventStore::Event
    • constructor takes named arguments:
    • 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

Custom mapper

Mapper is defined as a pipeline of transformations that transforms domain event object into record and back from record to domain event. I.e. Default is implemented as a set of 2 transformations. Transformation works on Record objects. Such Record 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 PipelineMapper needs to be given 1 edge transformation:

Extended implementation of Default with explicit default arguments:

module RubyEventStore
  module Mappers
    class Default < PipelineMapper
      def initialize(events_class_remapping: {})
        super(Pipeline.new(
          Transformation::EventClassRemapper.new(events_class_remapping),
          Transformation::SymbolizeMetadataKeys.new,
          to_domain_event: Transformation::DomainEvent.new
        ))
      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(record) & load(record). Both methods take a RubyEventStore::Record and return new instance of it, transformed. Transformation shall not modify given record and shall return new instance as a result.

If you use different class to represent domain event you shall also define edge transformation for your pipeline definition.

Example of custom mapper build based on mappers pipeline:

require 'msgpack'

class MessagePackSerialization
  def dump(record)
    RubyEventStore::Record.new(
      event_id:   record.event_id,
      metadata:   record.metadata.to_msg_pack,
      data:       record.data.to_msg_pack,
      event_type: record.event_type,
      timestamp:  record.timestamp,
      valid_at:   record.valid_at
    )
  end

  def load(record)
    RubyEventStore::Record.new(
      event_id:   record.event_id,
      metadata:   MessagePack.unpack(record.metadata),
      data:       MessagePack.unpack(record.data),
      event_type: record.event_type,
      timestamp:  record.timestamp,
      valid_at:   record.valid_at
    )
  end
end

class MyHashToMessagePackMapper < RubyEventStore::Mappers::PipelineMapper
  def initialize
    super(RubyEventStore::Mappers::Pipeline.new(
      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')