One event to rule them all
… and check why 5600+ Rails engineers read also this
One event to rule them all
Today I was asked a question:
How to ensure that domain event published by one of the aggregates is available in another aggregate?
The story is simple. The user, who is a member of some organization, is registering a new fuckup (maybe by using Slack command).
The fuckup is reported by just entering its title. It is reported in the context of the organization where the user belongs.
The rest is not important here but what we want to achieve is: modify a state of an organization
aggregate and,
at the same time, create new fuckup
aggregate.
This is quite simple to implement using Rails Event Store gem.
First let’s start with the definition of a command that is executed when fuckup is registered and a domain event that is published when fuckup is reported.
ReportFuckupCommand = Struct.new(:organization_id, :title)
FuckupReported = Class.new(RailsEventStore::Event)
Now we need a handler for our command. It should load an Organization
aggregate from event store, execute domain logic responsible for reporting
new fuckup and store all published domain events in the event store.
module ApplicationServices
class OrganizationService
def repository
event_store = RailsEventStore::Client.new.tap do |es|
es.subscribe(
ApplicationServices::OnFuckupReported.new,
['FuckupReported'])
end
@repository ||= RailsEventStore::Repositories::AggregateRepository.new(
event_store)
end
def report_fuckup(command)
org = Organization.new(command.organization_id).tap do |aggregate|
repository.load(aggregate)
end
org.report_fuckup(command.title)
repository.store(org)
end
end
end
Our command handler needs an Organization
aggregate. It should have all logic needed by the organization, does not matter now what it could be.
One thing to notice is that Organization
does not create new Fuckup
aggregate. Instead it “just” publishes a FuckupReported domain event.
module Domain
class Organization
include RailsEventStore::AggregateRoot
def initialize(id = SecureRandom.uuid)
@id = id
@public_fuckups = false
end
def report_fuckup(fuckup_title)
apply FuckupReported.new(data: {
title: fuckup_title,
organization_id: id,
public: public_fuckups })
end
attr_reader :id
private
attr_reader :public_fuckups
def apply_fuckup_reported(event)
# change organization state here...
end
end
end
So, how is Fuckup
created? The answer is: by handling a domain event. The event handler should create new Fuckup
aggregate (because we don’t have any to load it from event store) and just store it.
module EventHandlers
class OnFuckupReported
def repository
@repository ||= RailsEventStore::Repositories::AggregateRepository.new(
RailsEventStore::Client.new)
end
def hanle_event(event)
fuckup = Fuckup.create(event)
repository.store(fuckup)
end
end
end
module Domain
class Fuckup
include RailsEventStore::AggregateRoot
def initialize(id = SecureRandom.uuid)
@id = id
end
def self.create(event)
fuckup = Fuckup.new
fuckup.apply event
end
attr_reader :id
private
attr_reader :title, :organization_id, :public
def apply_fuckup_reported(event)
@title = event.data[:title]
end
end
end
With that implementation, our action responsible for reporting a fuckup should only execute our command handler. Both aggregates have the domain events stored in its own stream, however as you may notice by comparing event_id this is still the same domain event.
=> [#<RailsEventStore::Models::Event:0x007fc0c0191120
id: 77,
stream: "42d98fe4-6d50-4fda-8b7f-9575d9ffa5a1",
event_type: "FuckupReported",
event_id: "fc1703ef-ca43-4e20-ae0c-25969511e48a",
metadata: {:published_at=>2016-01-26 23:33:48 UTC},
data: {:title=>"test", :organization_id=>"42d98fe4-6d50-4fda-8b7f-9575d9ffa5a1", :public=>false},
created_at: Tue, 26 Jan 2016 23:33:48 UTC +00:00,
updated_at: Tue, 26 Jan 2016 23:33:48 UTC +00:00>,
#<RailsEventStore::Models::Event:0x007fc0c0190db0
id: 78,
stream: "b45d8738-1113-4952-a057-acb5344973c0",
event_type: "FuckupReported",
event_id: "fc1703ef-ca43-4e20-ae0c-25969511e48a",
metadata: {:published_at=>2016-01-26 23:33:48 UTC},
data: {:title=>"test", :organization_id=>"42d98fe4-6d50-4fda-8b7f-9575d9ffa5a1", :public=>false},
created_at: Tue, 26 Jan 2016 23:33:48 UTC +00:00,
updated_at: Tue, 26 Jan 2016 23:33:48 UTC +00:00>]