Reputation: 37
In Event Sourcing, do Events written to the system ever describe the changes to multiple objects? I'm trying to understand how one could log changes across N items without risking concurrency problems due to other writers.
Background
I have two types of settings in my system: global settings, and local settings.
There is one and exactly one Global settings object. This is controlled by a small set of Admins in the system. There are N "local" settings objects. These all inherit their available options from the Global settings. These are used by moderators to control options on their pages.
To further clarify, the Global settings object specifies what is inside of the local settings. e.g.
+------------------------+ +----------------------------+
| GLOBAL | | LOCALL |
| | | |
| Avalable Attributes: | | |
| custom_css: show | | custom_css: enabled |
| something_else: show | | something_else: disabled |
| | | |
+------------------------+ +----------------------------+
So, attributes declared in the Global Settings are essentially like feature toggles for what the Local settings can configure. As such, and this is the core of my problem, a change in the Global Settings must cascade down to N Local settings. The cascading change is a hard requirement due to audit requirements (Who changed what/when). i.e. moderators must be able to see why an option they had previously hasn't been removed.
In a traditional setup, this would be done in a transaction. But how would you do this in Event Sourcing without being able to log Events for different items atomically? Without transactions, we'd have the problem of Local Settings (potentially) being modified by a moderator while writes from the Admin to the Global config were still being written to the log.
Thus, the log would show a Local setting enabling an option that they shouldn't have because it was removed.
+----------------------------+------------------------+------------+
| User: Admin | User: bob(moderator) | INVALID |
| RemovedOption: custom_css | custom_css: True | STATE!! |
+----------------------------+------------------------+------------+
The only ways I can think to get around this is:
A. write a composite event that describes the "Transaction"
+----------------------------------------+
| SettingsChangedCompositeEvent |
| ----------------------------- |
| events:[ |
| {Global: {RemovedOption: custom_css}}, |
| {Local1: {RemovedOption: custom_css}}, |
| {Local2: {RemovedOption: custom_css}}, |
| ... |
| {LocalN: {RemovedOption: custom_css}} |
|
| ] |
| |
+----------------------------------------+
This seems totally wrong, as it would require a completely different way of processing things (at least according to my current view of how things are usually done). When Hydrating a local setting, I'd have to have special logic which can pluck the relevant change (if present) from the list of changes on the event and apply it conditionally.
B. Don't try to cascade the writes at all, but instead merge them at read time
Stop trying to keep N Local settings object in sync with one Global one, and instead always load the state of the Global settings along with the state of the Local Setting both for validation (is the thing I'm trying to toggle in Local still present in Global?), and for presentation (so that the final aggregate object is a combination of both Global and Local)
This setup, I think, suffers from the same concurrency problems as trying to keep everything in sync within the same log. i.e. A user could fire an event to update their local settings at the same time an admin user removes the option the event is acting upon.
C. Something Else?
I'm really unsure how to deal with data like this which is tightly bound together. What is the correct way to deal with data models that are basically inheritance based?
Upvotes: 1
Views: 635
Reputation: 4002
The first thing is to challenge your initial assumption, that using eventual consistency isn’t good enough.
You can create an event handler that listens for events from GlobalSettings, and then sends a command to each affected LocalSettings. So, eventually (normally quickly) the change is propagated to all the LocalSettings. If you make a local settings change in the short window between the original global change and it being propagated, the audit trail/event stream for the LocalSettings will show that change, then the GlobalSettings related event after. Even though the original GlobalSettings event happened first in the real world, you don’t need to show that, since that’s not the case for this particular aggregate. Also, the UI for saving the GlobalSettings change can in some way indicate when the change has been fully propagated if you want it to seem atomic to the user.
This is similar to the real-world scenario of a new business rule being told to a manager and then she takes a while to inform all her subordinates - it’s normal and not usually a problem as long as everyone gets the new rule quickly and then sticks to it.
The other option is to have just one settings event stream, and to use snapshots to make it manageable. However, this limits concurrency and scalability, so it depends on your requirements.
Upvotes: 1
Reputation: 57214
In Event Sourcing, do Events written to the system ever describe the changes to multiple objects?
Yes, potentially; it depends on how you model within the consistency boundary.
In domain driven design, Eric Evans talks about "aggregates"; there are lots of different and confusing explanations of what an aggregate is, but one of the core truths is this: all of the elements within an aggregate are stored together.
So, attributes declared in the Global Settings are essentially like feature toggles for what the Local settings can configure. As such, and this is the core of my problem, a change in the Global Settings must cascade down to N Local settings.
So to achieve the analog of changing all of these settings in one "transaction", you would design your model so that all of these are part of the same "aggregate", and you would store all of the events in the same event stream.
And that's going to be fine, if concurrent edits are rare. When concurrent edits happen, the usual answer is to have the loser in the race retry (automatically), and if that introduces a conflict then fail the command and allow the client to reload the state and work out what to do.
Within the stream of events, you can have events that describe changes to a single element of the system, changes that apply to all elements, changes that apply to a subset of elements, it's all good. Remember, events are just messages that describe a change of state -- you can make them as fine or coarse as makes sense for your domain.
When a single transaction is represented as multiple events, it is important that all or none of the events are written.
List<Event> oldHistory = eventStore.get(key)
List<Event> newHistory = doSomethingInterestingWith(oldHistory)
eventStore.compareAndSwap(key, oldHistory, newHistory)
Upvotes: 1