Wednesday, April 20, 2016

Shopping Carts and the Book of Record.

If I'm shopping for books on Amazon, I can look at my shopping list, click on a book, and have it added to my shopping cart.  For some items, Amazon will decline to add the item to my cart, and inform me, perhaps, that the item is no longer available.

At the grocery store, no matter how many times I click on the shopping list, the Fruit Loops don't appear in my cart.  I have to place the box in my cart by hand, next to the salad dressing that my phone says I can't put in the cart because it has been discontinued, and the milk that I can't put in my cart because it has expired.


If creating a user isn't a lot more fun than sending a command message to an aggregate, you are doing it wrong.


We often want representations of entities that we don't control, because the approximation we get by querying our representations is close enough to the answer we would get by going off to inspect the entities in question, while being much more convenient.

But if the entities aren't under our control, we have no business sending commands to the representations.  Our representations don't have veto power over the book of record.

Aggregates only make sense when your domain model is the book of record.





Which means that you have no ability to enforce an invariant outside of the book of record.  You can only query the available history, detect inconsistencies, and perhaps initiate a remediation process.



On Read Models

I learned something new about read models today.

Most discussions I have found so far emphasize the separation of the read model(s) from the write model. 

For example, in an event sourced solution, the write model will update the event history in the book of record.  Asynchronously, new events are read out of the book of record, and published.  Event handlers process these new events, and produce updated projections.  The read model answers queries using the most recently published projection.  Because we are freed from the constraints of the write model, we can store the data we need in whatever format gives us the best performance; reads are "fast".

But by the time the read models can access the data; the data is old -- there's always going to be some latency between "then" and "now".   In this example, we've had to wait for the events to be published (separately from the write), and then for the event handlers to consume them, construct the new projections, and store them.

What if we need the data to be younger?

A possible answer; have the read models pull the events from the book of record directly, then consume the events directly.  It's not free to do this -- the read model has to do its own processing, which adds to its own latency.  There record book is doing more work (answering more queries), which may make your writes slower, and so on.

But it's a choice you can make; selecting which parts of the system get the freshest data, and which parts of the system can trade off freshness for other benefits.

Example

In some use cases, after handling a command, you will want to refresh the client with a projection that includes the effect of the command that just completed; think Post/Redirect/Get.

In an event sourced solution, one option is to return, from the command, the version number of the aggregate that was just updated.  This version number becomes part of the query used to refresh the view.

In handling the query, you compare the version number in the query with that used to generate the projection.  If the projection was assembled from a history that includes that version, you're done -- just return the existing projection.

But when the query is describing a version that is still in the future (from the perspective of the projection), one option is to suspend the query until history "catches up", and a new projection is assembled.  An alternative approach is to query the book of record directly, to retrieve the missing history and update a copy of the projection "by hand".  More work, more contention, less latency.

If the client is sophisticated enough to be carrying a local copy of the model, it can apply its own changes to that model, provisionally; reconciling the model when the next updates arrive from the server.  That supports the illusion of low latency without creating additional work for the book of record (but might involve later reconciliation).