Rails Event Sourcing
This gem provides features to setup an event sourcing application using ActiveRecord. ActiveJob is necessary only to use async callbacks.
DISCLAIMER: this project is in alpha stage
The main components are:
- event: track state changes for an application model;
- command: wrap events creation;
- dispatcher: events' callbacks (sync and async).
This gem adds a layer to handle events for the underlying application models. In short:
- setup: an event model is created for each "event-ed" application model;
- usage: creating/updating/deleting application entities is applied via events;
- every change to an application model (named aggregate in the event perspective) is stored in an event record;
- querying application models is the same as usual.
â if you like it, please.
A sample usage workflow:
# Load a plain Post model:
post = Post.find(1)
# Update that post's description:
Posts::ChangedDescriptionEvent.create!(post: post, description: 'My beautiful post content')
# Create a new post:
Posts::CreatedEvent.create!(title: 'New post!', description: 'Another beautiful post')
# List events for an aggregated entity (in this case Posts::Event is a STI base class for the events):
events = Posts::Event.events_for(post)
# Rollback the post to a specific version:
events[2].rollback!
# The aggregated entity is restored to the specific state, the events above that point are removed
âšī¸ this project is based on the event-sourcing-rails-todo-app-demo proposed by Philippe Creux and his video presentation for the Rails Conf 2019 đ
Usage
- Add to your Gemfile:
gem 'rails-event-sourcing'
(and executebundle
) - Create a migration per model to store the related events, example for a User model:
bin/rails generate migration CreateUserEvents type:string user:reference data:text metadata:text
- Create the required events, example to create a User:
module Users
class CreatedEvent < RailsEventSourcing::BaseEvent
self.table_name = 'user_events' # usually this fits better in a base class using STI
belongs_to :user, autosave: false
data_attributes :name
def apply(user)
# this method will be applied when the event is created
user.name = name
# the aggregated entity must be returned
user
end
end
end
- Create an event (which applies the User creation) with:
Users::CreatedEvent.create!(name: 'Some user')
- Optionally define a create Command, for example:
module Users
class CreateCommand
include RailsEventSourcing::Command
attributes :user, :name
def build_event
# this method will prepare the event when the command is executed
Users::CreatedEvent.new(user_id: user.id, name: name)
end
end
end
- Invoke it with:
Users::CreateCommand.call(name: 'Some name')
Please take a look at the dummy app for a complete example.
In this case I preferred to store events models in app/events, commands in app/commands and dispatchers in app/dispatchers - but this is not mandatory. Another option could be to have an Events
namespace and a single event could be: Events::TodoItem::CreatedEvent
.
Examples
Events:
TodoLists::Created.create!(name: 'My TODO 1')
TodoLists::NameUpdated.create!(name: 'My TODO!', todo_list: TodoList.first)
TodoItems::Created.create!(todo_list_id: TodoList.first.id, name: 'First item')
TodoItems::Completed.create!(todo_item: TodoItem.last)
Commands:
TodoLists::Create.call(name: 'My todo')
TodoItems::Create.call(todo_list: TodoList.first, name: 'Some task')
Dispatchers:
class TodoItemsDispatcher < RailsEventSourcing::EventDispatcher
on TodoItems::Created, trigger: ->(todo_item) { puts ">>> TodoItems::Created [##{todo_item.id}]" }
on TodoItems::Completed, async: Notifications::TodoItems::Completed
end
# When the event TodoItems::Created is created the trigger callback is executed
# When the event TodoItems::Completed is created a job to create a Notifications::TodoItems::Completed event is scheduled
To do
- Generators for events, commands and dispatchers
- Database specific optimizations
- Add more usage examples
Do you like it? Star it!
If you use this component just star it. A developer is more motivated to improve a project when there is some interest.
Or consider offering me a coffee, it's a small thing but it is greatly appreciated: about me.
Contributors
- Mattia Roccoberton: author
License
The gem is available as open source under the terms of the MIT License.