Streams
Some authors suggest to classify the events in "streams", and many authors identify the "stream" with the "aggregate Id".
Say an event car.repainted
by which we mean we repainted the car with id 12345
into {color:red}
.
In this example the stream Id would probably be something like car.12345
or if you have universal unique ids, then just 12345
.
Some authors in fact suggest to store the event stream into a table with an structure more or less similar to the following (if you go with relational):
| writeIndex | event | cachedEventId | cachedTimeStamp | cachedType | cachedStreamId |
| 1 | JSON | abcd | xxxx | car.repainted | 12345 |
- The
event
column has the "original" value object of the event, most probably serialized to JSON if it's relational DB. - The
writeIndex
is just for DB administration and has nothing to do with the domain itself. You can "dump" your events into another DB and have writeIndex rewritten with no side-effects. - The
cached*
fields are for easily finding and filtering events and they all can be calculated from the event itself. - Of special mention
cachedStreamId
which will be used -according to some authors- to be mapped to the "aggregate Id to which the event belongs to". In this case, "car identified by12345
".
If you don't go with relational, you'd probably store your event "as a document" in a data-lake / event-store / document-warehouse / or-call-it-how-you-want (mongo, redis, elasticsearch...) and then you make buckets or groups or selections or filters to retrieve some events by a criteria (and one of the criteria is "what entity/aggregate Id I'm interested in" => streamId again).
Replaying
When replaying the events to create fresh projections you just have a bunch of subcribers to the event type (and probably version) and if it is for you, you read the full-original-document of the event, you process it, calculate and update the projection. And if the event is not for you, you just skip it.
When replaying, you restore the aggregate read-tables you want to rebuild to a known initial set (maybe "all empty"), then select one or more streams, select the events in chronological order and iteratively update the state of the aggregates.
Okey...
All this seams to me reasonable. No news until here.
Question
But... I have now some shortcircuit in my head... It's a so basic shortcircuit that probably the answer is so obvious that I'll feel silly to not being able to see it now...
What happens... if an event is "equally important" to two aggregates of different types (assuming they are inside the same bounded context) or if even it refers to two instances of the same aggregate type.
Example of 2 equally-important different aggregates:
Imagine you are in the train industry and you have those aggregates:
Locomotive
Wagon
For one moment just imagine that one locomotive can carry 0 or 1 wagon but not many wagons.
And you have those commands:
Attach( locomotiveId, wagonId )
Detach( locomotiveId, wagonId )
Attach can be rejected if locomotive and wagon were already attached to something and Detach can be rejected if the command is issued when they are not attached.
The events are obviously the corresponding ones:
AttachedEvent( locomotiveId, wagonId )
DetachedEvent( locomotiveId, wagonId )
Q:
What's the stream id there? both loco and wagon are of equal importance, it's not an event "of the loco" or "of the wagon". It's an event of the domain that affects those two! Which one is the streamId and why?
Example with 2 aggregates of the same type
Say an issue tracker. You have this aggregate:
Issue
And these commands:
MarkAsRelated( issueAId, issueBId )
UnmarkAsRelated( issueAId, issueBId )
And mark is rejected if the mark was already there and unmark is rejected it there was not any previous mark.
And those the events:
MarkedAsRelatedEvent( issueAId, issueBId )
UnmarkedAsRelatedEvent( issueAId, issueBId )
Q:
Same question here: It's not that the relationship "belongs" to issue A or B. Either they are related or not. But its bidirecional. If A is related to B then B is related to A. What's the streamId here and why?
History is written once
In any case, I don't see creating TWO events one for each. That's a matter of the calculators...
If we see the definition of "history" (not in computers, in general!) it says "a sequence of events that happened". In the free dictionary it says: "A chronological record of events" (https://www.thefreedictionary.com/history)
So when there's war between social group A and social group B and say B beats A, you do not write 2 events: lost(A)
and won(B)
. You just write one event warFinished( wonBy:B, lostBy:A )
.
Question
So how do you handle event streams when the event affects multiple entites at the time and it's not that it "belongs" to one and the other is a complement to that but it's truly equal to both?