php-ddd
php-ddd copied to clipboard
Enriching / Fat Events and Event Handler / Projector - TELL, DON'T ASK
I have an Inspection Domain Model with a lot of properties, some of them nested.
A new Inspection is handled by a Command Handler. The Inspection AR then raises an ResultRecorded Event.
Currently this event only holds the AR InspectionId. Not all of the properties I mentioned.
Then an Event Handler catches the event. Currently the Handler lives inside the same Bounded Context "Inspection".
The Handler fetches the (WRITE) Domain Model from its repository by the InspectionId. It then transforms ("projects"?) the WRITE Model to the READ Model by passing the data to a "fromInspection" constructor. This constructor then has to do some heavy projection because of the nested Aggregates.
Finally the Event Handler stores the READ Model.
If I get the term correctly the Event Handler can be regarded as the "Projector"? https://abdullin.com/post/event-sourcing-projections/
Then I asked myself what the responsibilities of a Projector is. I think it should do what every Application (Handler) should do: Receive a message, transform it into Domain language if required (e.g. convert primitive types to Value Objects) and then pass it to the repository and return nothing.
So far this seems to be covered by my example. Though I didn't feel sure if the "transformation" inside the constructor should be moved to the Event Handler.
Then I read about "enriching" Events by @Lavinski: https://www.lavinski.me/generating-read-models-with-event-sourcing/
Also mentioned as "Fat Events" by @mathiasverraes: https://speakerdeck.com/mathiasverraes/practical-event-sourcing?slide=41
This made me rethink the process of the Event Handler / Projector. What if the Projector was living in a different Bounded Context?
- Should it really take the InspectionId to fetch the WRITE model from the other context? What if I had multiple READ models?
- Should my READ model(s) really have a constructor taking the "rich" data from the WRITE model?
Conclusion: NO! NO! And NO!
The Projector - different bounded context or not - should not receive the Identifier of the original WRITE model to fetch it. Instead it should receive all data it needs - even it is a lot of data. Optionally the event could be extended or the data could be devided into multiple Events the Subscriber can listen to(o):
- http://stackoverflow.com/a/12343171/1937050
I would move the transformation inside the READ model constructor to place where the Event is raised - the (WRITE) Domain Model.
This felt strange at first! Why? It felt like setting up a data structure the the Domain Model should not know about BUT the Subscriber e.g. the Projector or the READ Model in the end. But I guess this type of thinking is simply wrong! We are NOT ASKING what somebody needs - WE ARE TELLING!
What do you think? Code examples will follow!
Here is an excerpt of the constructor transforming the data of the Domain (WRITE) Model data into the READ model:
public static function fromBundle(Inspection $bundle)
{
$faults = [];
foreach ($bundle->parttypeStateNokProcessed()->faultQuantities() as $faultQuantity) {
$faults[] = new NokResultFault(
new FaultId($faultQuantity->faultId()),
$faultQuantity->area(),
$faultQuantity->quantity(),
$faultQuantity->comment()
);
}
$nokProcessed = new NokResult(
$bundle->parttypeStateNokProcessed()->quantity(), $faults
);
return new static(
BundleId::create($bundle->id()), $bundle->contractId(), $bundle->date(), $bundle->locationId(), 'todo',
$bundle->shift(), $bundle->shippingnumber(), $bundle->comment(),
$nokProcessed
);
}
As you can see there are nested aggregates. But these really belong the AR. I wonder should I pass them as Value Objects or use primitive types instead?
This process should then be used in order to enrich the "ResultRecorded" event:
class Inspection implements ContainsRecordedMessages
{
use PrivateMessageRecorderCapabilities;
public static function recordResult(...)
{
$this->record(new ResultRecorded(InspectionId::create($this->id)));
}
}
Another thought:
Should I move the data transformation for the Event to an Assembler to return the Event as a DTO?
@webdevilopers did you see my follow up post https://www.lavinski.me/event-sourcing-what-properties-should-domain-events-have/
Great follow up @Lavinski !
Unfortunately it is hard to relate to my example. As you can see I need to almost copy the original AR to create the READ model inside the other BC.
You suggest to use multiple events. The only possibility I found was using one new Event for each faultQuantities I'm adding see https://github.com/webdevilopers/php-ddd/issues/28#issuecomment-274053015.
My other bounded context uses a MongoDB document store. I would need an initial Event to create the inspection. Then the other Events would be handled and each faultQuantity would be added to that initial document.
Do you have any better ideas for this use case?
From what I know you need enough events to able rebuild the AR's state in memory. So copying the entire AR is not uncommon.
Collections however are a bit more challenging, you can create events for each add/delete (and update, which in fact is an add/delete) but from my limited experience this will generate allot of events where a CollectionChanged event is much more easy to handle but also less precise.
Thanks @sstok !
So copying the entire AR is not uncommon.
That's good to know!
I wonder what if I went back to my initial approach and only pass the AR Id then? The other bounded (READ) context could then get the details via REST for instance from the initial (WRITE) context.
Or is this too much dependency on other bounded contexts / sources?
I like the answers by @yreynhout here:
An appointment is rescheduled from one schedule to another. The resulting event AppointmentRescheduledEvent contains both the schedule I rescheduled from and the schedule I rescheduled to. It makes it easier on my consumers (they don't have to figure out where it was rescheduled from by querying their own state (if they keep any state at all)). Because the appointment aggregate knew in which schedule it was before the reschedule command, it was very easy to enrich. Do I use the AppointmentRescheduledEvent.FromSchedule property when restoring (replaying) the state of my appointment aggregate? No. So what?
- https://groups.google.com/d/msg/dddcqrs/Xq1H2zJh3LU/sx_7406Cj6AJ
The most basic way we do it is using the command handler as Ramin suggests:
Have the command handler query the read model for the ProductDetailedDescription and send that information inside the method call to the domain along with the other information such as the ProductId.
- https://groups.google.com/d/msg/dddcqrs/Xq1H2zJh3LU/QX9GSF4WabAJ
Of course we only do this to enrich the event for a consumer when the aggregate itself does not contain the data.
We do use enrichment plugins for Authorization details e.g. userID etc. - but not for domain details. These are pulled e.g. in the command handler before being passed to the aggregate. THEN the aggregate will have all data for the events. Some data is required for the aggregate state and some maybe for other consumers later.
If you don't like to create too many properties on your aggregate the DDD red book suggests to move them to separated value objects e.g. instead of "OfferTitle" and "OfferPreword" you could group them to a "OfferHeader" value object. At least when moving the data to the event itself.
Someone suggested to add the data to the command already.
We prefer the command handler. If a "company name" is required and a "company ID" is given in the command the handler can also check the existence of the company on the read-model-repository or otherwise reject the command.