February 24, 2022
Recently we made an introduction to the Rails Event Store but to get fully into the topic there are dozens of ideas to learn about, so we won’t stop there. Today I would like to tackle the simplest problem in the stack - Domain Events.
Domain Event: a data structure representing one domain change
An event is something that has happened in the past
Domain Events Design Implementation
You can find a lot of definitions on the web and based on them we can conclude that event is:
Let’s discuss each one in short.
An event describes something that happened in the past. It’s a fact and you cannot argue with it. When a new event comes in you don’t have to validate it - it’s rather a contrary, you have to accept it fully, no matter if you’re ready for the truth or not. Because an event represents the truth. You cannot erase it and if you don’t like the fact you can either ignore the event or raise a new one that will change the current state into the desired one.
Technically speaking, an event is as simple, as struct is. To put it simply it’s a named bunch of data. In an extreme case, it might be just a name, without any data, but in most cases, an event will carry on few ids, changed values and that's it. As you can see, an event is also a very simple struct.
A struct means no logic, no additional methods, just plain data, and getters.
Cause the event is a fact, you cannot change it, so there are only getters and no setters.
An event represents change. Something was updated, moved, increased, turned off, switched, removed, selected, assigned - the vocabulary is full of verbs that can describe the change. If you struggle to find a proper name you can also ask for help on a thesaurus.
Now, when we already covered what is an event, let’s discuss a few issues that you might encounter when you decide to start implementing them.
There are only two hard things in Computer Science: cache invalidation and naming things.
- Phil Karlton
Thankfully, there is a well-defined convention for naming events - noun + verb in the past tense. I like to follow a recommendation from Mathias Verraes:
I recommend using natural language and making small sentences, such as “Stock was depleted”, instead of newspaper-style shortcuts such as “Depleted stock”. In my experience, this greatly improves communication and understanding.
so it’s a noun + was + verb for me but it’s a cosmetic detail and matter of personal preference. Few examples for event names, to make it more clear:
The best place to find proper nouns and verbs is in the business you are modeling. Such vocabulary creates something called ubiquitous language, and it’s a topic that deserves its own series of posts.
One of the most tricky issues that you might have at the beginning of an adventure with events is when it comes to deciding how big or small your event should be.
Should I be more explicit and raise small events like
EmailWasUpdated? This approach allows us to be more flexible - you can create handlers that will react only to a piece of very specific information that is needed in the process. On the other hand, you will probably end up with much more code to write, and - in case of a lot of changes in a single request - it could mean a lot of unnecessary hits to the database.
# @param [Events::FirstNameWasChanged]
An obvious alternative is to go big, like
ProfileWasChanged, so you can handle a lot of things in one hit. The problem is, that now your handlers probably need some routing, they have to check what kind of data the event provides. You might have to deal with
nulls and decide if
null means “nothing changed” or “clean the value” but then you will have to send redundant data that haven’t changed but you don’t want to remove them.
# @param [Events::ProfileWasChanged]
user = User.find(event.data.user_id)
# nil means "nothing changed"
user.first_name = event.data.first_name if event.data.first_name
# nil means "clean the value"
# so we have to send the value no matter if it changed or not
user.last_name = event.data.last_name
What’s worse, big events are brittle to refactorings and changes to the event schema, so you have to learn how to make extra steps and how to deal with such cases (btw - there is a great book that covers these issues).
So far I have three simple heuristics that help me decide which way to go:
You can read more e.g. on the sapiens works blog or listen to a better software design podcast (in Polish).
Your events should carry on some data, i.e.:
The timestamp is a no-brainer - it’s good to know when the event happened and libraries like Rails Event Store keep you covered here out of the box. Although timestamp is not a domain related detail, it might be a good idea to keep it separated, e.g. as metadata, along with a request IP address and
user_id who triggered the action.
Changed data probably also don’t need a separate explanation - you want to track what exactly happened and changed so you have to put details into the event, like a new name, updated price, etc.
Let’s imagine that we have three entities -
Buyer. When you add a product to the cart it would be obvious to add
cart_id to the related event. But should you also add
buyer_id? Since this introduces a little redundancy -
buyer_id might be tracked thanks to
cart_id - you might be tempted to save some space but I would argue against such optimization. Keeping all related aggregate ids in an event will often simplify your handlers (no need to query for the
buyer if you need to pass a
buyer_id in further actions) but more importantly - it would give you more flexibility for filtering streams, e.g. when you want to process all events related to the buyer.
Last but not least, we have to remember that the only constant thing in the world is change. And so, your requirements may change, your understanding of the domain may change or you simply may want to introduce the change to fix a bug. And such change might require changing the struct of an event which is not as simple as it might sound - as you remember events are immutable facts and we cannot update the history. There are different strategies how to deal with this problem (I would like to recommend Greg Young’s book again) but most of them are based on keeping the version number of a struct in the event. The version might be hardcoded in the event name, e.g.
SomethingImportantHappenedV1 but personally, I prefer to keep it in the event metadata.
Rails Event Store by default provides schema-less events. Such event might be implemented with a single line, e.g.
FooHappened = Class.new(RailsEventStore::Event). This approach is great for learning and experimentation purposes but we don’t recommend it for production. Lack of schema means your events will accept any hash you will provide and that will kick your butt with unexpected
nil, typos, overlooked fields, etc in the worst possible moment.
The sky is the limit when it comes to implementing a format for a strict schema for events but probably the most popular approach goes with extraordinary dry-struct library and Rails Event Store also provides an example of possible implementation, which will end up with events defined as:
class FooHappened < Event
attribute :foo_id, Types::UUID
attribute :value, Types::Integer
Sooner or later you will realize that your system actually contains two different kinds of events - internal and private to a specific context, which most often you want to keep small. And often bigger events for intercommunication between different contexts or systems. Such events are often called integration events or summary events. The implementation relies on special processes that listen to internal events, collect data, and when specific conditions occur, they create a new integration/summary event that might be sent to another context or system.
My personal greatest benefit of events is the influence on the way I think about designing the system. The moment when you have to name the fact that actually happens opens a magic box in your head that unveils edge cases, impacts on other parts of the system, or unknowns that you have to ask the client. It really changes the way you think and communicates with others. It shapes your language.
Not to mention that now you have an audit log for free.
It’s true, that full implementation of Domain-Driven Design with CQRS and Event Sourcing is not simple. It requires knowledge, a lot of learning about new patterns (like how to deal with GDPR), experience, and still in some cases it may be unnecessary.
Still, emitting events is so simple (at least with Rails Event Store) that it’s almost for free, and the benefits are so great that it’s really hard for me to find a reason to not do so. You can simply start with
event_store.publish(SomethingHappened) and learn about all this complex stuff later, piece by piece, step by step at your own pace without sacrificing anything.
Thank you for reading and see you next time!
Development Ruby/Rails OutsourcingHow to Prepare to Work With an External RoR Development Agency