Introduction to event sourcing in Ruby on Rails | ASSIST Software Romania
get in touch
>

LIKE

SHARE

Facebook Share Tweet LinkedIn Share

FOLLOW

LinkedIn Follow Xing Follow
Cioata Petru

Head of Ruby Development at ASSIST

„Nothing is too small to know and nothing too big to attempt.” - William Cornelius Van Horne
 

Andrei Afrasinei

Software Development Engineer III at ASSIST

"My mind is sharper than any blade" - Dark Seer

Read time: 10 minutes

1. Introduction

A large number of applications usually have a database where all kind of data is kept while being processed and displayed to the user. Although for the large majority of these apps only the current state of data is important, there are some cases where a detailed history of all the state changes is needed in order to find out how we got to the current state.

In this case, when the state change history is important, we can use the event sourcing pattern, the design pattern that ensures that each step was logged and can be accessed in order to check all the steps involved until the current state.
 
Fig 1. Event Source Rails

Event sourcing (ES) is a design pattern that allows us to build applications that are oriented around domains that are easy to extend. An event can be described as an important change of a certain state. For example, when a user moves a product from point A to point B, the product’s location changes from A to B.

Many applications would just store the current state (the location in our case) of the product but event sourcing allows us to keep track of all the changes made to an object’s state. Let’s say a user moves the same product from point B to point C. In this case, we usually only know the current state (point C) but using event sourcing in rails, we know that the product was moved from point A to point B before reaching point C. Event sourcing in rails Store is a gem that allows us to use an Event-Driven Architecture (EDA) for our Rails applications. 

 

2. Application concept

In this article, we want to show a use case of the event sourcing in Ruby on Rails pattern so we have created an application that is available on GitHub. Based on the supposition that a company has multiple entities (phones, computers, keyboards, etc.) and it keeps all this data in Excel files, the requirement is to create an application that can be used to store all the data and all the states every entity goes through.
 
 Fig 2.  Entities list
The main objective of this app is to keep track of the entities in a company. We would like to be able to answer questions like: “where was a certain keyboard over the last 5 months?” (from a store to person X, then being broken, back to the warehouse and so on) or “which entities were deleted so far?” and so on.

The app should be able to store data about every entity, even if this data is modified from one stage to another. Let’s say a computer is given to an operator with certain specifications but for some reason, it had added/removed a RAM card, so when it’s moved we can keep track of these changes as well.
 

3. How to configure rails_event_store inside a Rails application

Add gem "rails_event_store" in Gemfile.rb and run bundle.
spring stop # if you use spring
rails generate rails_event_store_active_record:migration
rails db:migrate
Then instantiate a client by adding the code below in your config/application.rb file:
config.event_store = RailsEventStore::Client.new
Add the code below in your config/application.rb file because we will use the AggregateRoot module later:
AggregateRoot.configure do |config|
 config.default_event_store = Rails.application.config.event_store
end
Your application.rb file should look approximately like this:
require_relative 'boot'

require 'rails/all'
require 'aggregate_root'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module RailsEventstoreEntitiesTracker
 class Application < Rails::Application
   # Initialize configuration defaults for originally generated Rails version.
   config.load_defaults 5.2
   config.autoload_paths += Dir["#{config.root}/app/**/"]
   config.autoload_paths += Dir["#{config.root}/lib/**/"]

   config.before_configuration do
     env_file = File.join(Rails.root, 'config', 'local_env.yml')
     YAML.load(File.open(env_file)).each do |key, value|
       ENV[key.to_s] = value
     end if File.exists?(env_file)
   end

   config.event_store = RailsEventStore::Client.new
 
   AggregateRoot.configure do |config|
     config.default_event_store = Rails.application.config.event_store
   end
 end
end
Then we need to add an event_store.rb file under the config/initializers folder where we will define our subscribers to the events. 
Rails.application.config.event_store.tap do |es|
	#more later
end
After that, we need to create some files under the lib/commands/ folder. Firstly we will add the validation_error.rb file so we can use customized errors:
module Commands
 class ValidationError < StandardError
   attr_accessor :message, :custom_error

   def initialize(message, custom_error = nil)
     @message = message
     @custom_error = custom_error
   end
 end
end
Secondly, we will add the base.rb file: 
module Commands
 class Base
   include ActiveModel::Model
   include ActiveModel::Validations
   include ActiveModel::Conversion

   def initialize(attributes={})
     super
   end

   def validate!
     raise ValidationError, errors unless valid?
   end

   def persisted?
     false
   end
 end
end
All our commands will inherit from this Base class so it can be validated and initialized. In this article, we are only going to present the Create event, but all other events that were implemented - Edit, Delete, Restore - can be checked on GitHub.
 
Finally, we will add the execute.rb helper file, which will help us to use the execute method in our controllers.
module Commands
 module Execute
   def execute(command)
     command.validate!
     handler_for(command).call(command)
   end

   private
   def handler_for(command)
     {
       Commands::Entities::CreateEntity => CommandHandlers::Entities::CreateEntity.new,
       Commands::Entities::EditEntity   => CommandHandlers::Entities::EditEntity.new,
       Commands::Entities::DeleteEntity => CommandHandlers::Entities::DeleteEntity.new,
       Commands::Entities::RestoreEntity => CommandHandlers::Entities::RestoreEntity.new,
     }.fetch(command.class)
   end
 end
end

In event sourcing in rails, an event is seen as an action that already happened, therefore, we need to represent it as a verb in the past tense such as EntityCreated, EntityEdited, EntityAddedToObject. Current production databases mostly rely on storing the current state of a record while the event store stores every event in the database.
module Events
 EntityCreated = Class.new(RailsEventStore::Event)
 EntityEdited = Class.new(RailsEventStore::Event)
 EntityDeleted = Class.new(RailsEventStore::Event)
 EntityRestored = Class.new(RailsEventStore::Event)
end
After we have defined the events, the next step would be to define what the action does. This action represents the command. This way we decouple the events that are done from the action. As can be seen in the screenshot below, we created a new module under lib, called commands where all the commands are defined. In order to be saved, each action (command) needs to be validated and checked if it is persisted or not.
Fig 3. Commands and command handlers file structure
For example, `CreateEntity`, defined below represents the definition of create action. As it can be seen it validates the presence of uid and name, and it checks that they are not blank.
 
create_entity.rb file:
module Commands
 module Entities
   class CreateEntity < Base
     attr_accessor :uid, :name, :description, :state, :extra_data

     validates :uid, presence: true, allow_blank: false
     validates :name, presence: true, allow_blank: false

     def aggregate_uid
       uid
     end
   end
 end
end
After the command is executed, a command handler is triggered which will manipulate data from the command and it will call methods on the domain model. The command handler is an entry point to your domain. 
 
create_entity.rb handler file:
module CommandHandlers
 module Entities
   class CreateEntity
     def call(command)
       stream = "Domain::Entity$#{command.aggregate_uid}"
       entity_data = {
         uid: command.uid,
         name: command.name,
         description: command.description,
         state: command.state,
         extra_data: command.extra_data
       }

       aggregate = Domain::Entity.new(command.aggregate_uid).load(stream)
       aggregate.create(entity_data)
       aggregate.store
     end
   end
 end
end
When using event sourcing in rails, your aggregates (domain models) are built based on domain events. To create a new aggregate domain object, we need to include the AggregateRoot module inside domain classes. 
module Domain
 class Entity
   include AggregateRoot
...
Its underlying data model doesn’t store the current state, but instead a series of domain events that have been applied to that aggregate since the beginning of its creation. The convention proposed by the rails_event_store gem is to use the word apply + underscored event class name:
module Domain
 class Entity
   include AggregateRoot

   attr_reader :uid, :name, :description, :state, :extra_data, :restored

   def initialize(uid)
     @uid = uid
   end

   def create(data)
     apply Events::EntityCreated.new(data: data)
   end

    private

   def apply_entity_created(event)
     @name = event.data[:name]
     @description = event.data[:description]
     @state = event.data[:state]
     @extra_data = event.data[:extra_data]
   end
 end
end
Using this convention will ensure that the aggregate state will not be exposed and it will allow it to protect its invariants.
 
Every method called by an aggregate will most likely result in publishing new events. 

Note: The only way to change the internal state of an aggregate is by publishing and applying new domain events, to ensure that the aggregate will be rebuilt to the same state from the events.
 
Once an event is published, it will trigger a subscriber defined in our event_store.rb file:
Rails.application.config.event_store.tap do |es|
 es.subscribe(Denormalizers::Entities::EntityCreated, to: [Events::EntityCreated])
end
We can add a denormalizer that will manipulate data from the event and insert it into a classic database, which will be used for displaying data (GET requests). A huge benefit of this is that we can get the latest state of an entity in the traditional way without browsing in the stream, therefore when an event in rails is published we take the data and store it in its corresponding table.

 

Example:

module Denormalizers
 module Entities
   class EntityCreated
     def call(event)
       ::Entity.create!(event.data)
     end
   end
 end
end

3.1 How are data organized?

Each entry into the ES database contains a list of attributes as described in the following paragraphs. There are similarities between the ES database and an ordinary database such as the ID or metadata. When we install the event store gem, it provides us two tables, event_store_events and event_store_events_in_streams.
 
The `event_store_events` table saves the events in ruby on rails and contains the following attributes: ID, event_type, data, created_at and metadata.
The ID field is unique for each entry, the event_type field is the event class name (Events:: + Event Name), the data field is everything that is passed to the domain when an event is applied, and the created_at field is the timestamp.
 
Similar to an SQL database, the event store provides the option to store a metadata field as well. By default, it stores the remote ip (which is the IP of the HTTP client that issued the request) and the request id (which is a unique id of the request) but it can be extended to store custom data according to the requirements of the application. Data and metadata fields are blob types.
Fig 4. event_store_events entry sample for EntityDeteled event
Fig 5. Metadata text sample for EntityDeteled event
Fig 6. Data text sample for EntityDeteled event
 
The `event_store_events_in_streams` table is used to save the streams and has the following attributes: ID, stream, event_id, created_at and position. The id field is an incremented ID, stream is the name of the stream which conventionally is Domain:: + Domain Model Name + $aggregate_uid, event_id is the unique id of the event, the created_at field is the timestamp, and the position is the expected version of the stream, but we are not going to cover that in this article. 
 
Fig 7. event_store_events_in_streams entry sample
The app we created for this article provides four events - EntityCreated, EntityEdited, EntityDeleted, and EntityRestored. 
Fig 8. Events list for an entity
Fig9. Event list of a restored entity
We wanted to keep track of all the deleted entities so we used another important tool provided by the gem - link to another stream. When an event is published it can live in multiple streams, this being very powerful when we want to group events with specific particularities. So we linked the EntityDeleted event to another stream called DeletedEntities
class DeletedEntitiesTracker
 def call(event)
   Rails.application.config.event_store.link(
     event.event_id,
     stream_name: "DeletedEntities"
   )
 end
end
Fig 10. Event list of a deleted entity

4. Conclusions

There are cases in some applications where the user is interested in knowing more than the current state of a certain piece of data. In these cases, event sourcing in ruby on rails design is the perfect fit and for a Rails application, we have the `rails_event_store` gem, which is the best choice to do it. 
 
Our article tried to showcase what the event sourcing in rails pattern is and how it can be used in a Rails application. Although we covered only the creation event, the other events and the entire code base for the application can be checked on GitHub.
 
The application contains a number of specs as well and you can find out even more about how to integrate cucumber tests with Rails Event Store by reading our colleague’s article on this topic.

Vous souhaitez nous contacter ? 

Si vous êtes intéressés par nos services de développement de logiciel, si vous souhaitez rejoindre notre équipe, ou si vous souhaitez tout simplement en savoir plus sur nous, nous sommes à votre disposition. Contactez-nous ! Écrivez-nous et un membre de l'équipe ASSIST vous répondra dans les plus brefs délais. Nous pourrons certainement vous ASSISTer.

CONTACTEZ-NOUS