Patterns for Decoupling in Distributed Systems: Summary Event

Instead of emitting a stream of Domain Events, emit a single Summary.

By Mathias Verraes
Published on 08 May 2019



Summary Event

Instead of emitting a stream of Domain Events, emit a single Summary.

Problem

A business process involves a number of steps that each produce domain events. A consumer depends on information in those events, and listens to all of them to make meaningful decisions. This in itself is perfectly fine, especially when it’s only a small number of event types. When the number of event types is large, we can get concerned about the amount of knowledge the consumer now has of the producer’s business process. The consumer is now almost like a micromanager, looking over the shoulder of the producer and getting immediate reports of everything the producer does.

This has a risk: when the original business process evolves, event types might be added, removed, or altered. This requires a change in the consumer, which needs to be adapted to the new types, and either be deployed at the same time or have a good versioning strategy.

And it gets worse if there are multiple consumers, that all need to be aware of every type of event the producer emits. There’s often also quite some duplication: each consumer projects similar or identical state off of the events.

Solution

Instead of emitting each event, the original business process can keep all the events private. At the end of the process, the process emits a single Summary Event. This event is redundant, in the sense that it contains only information that was already available in the preceding events. Consumers, instead of being aware of every event, are now only listening to this Summary Event, which tells them everything they need to know, with little to no irrelevant (to them) information. Consumers do not need to track state changes during the process, because they get everything at the end, and can then act on them.

You can recognize opportunities to apply the Summary Event pattern, by looking at the consumer. If the consumer listens to multiple event types, but there’s only one event type that causes the consumer to do some meaningful domain action (besides keeping state), it’s a good sign. The producer offers clues as well: the business process involves creating some kind of artefact, and the business value is not necessarily in the artefact itself, but in the usage of the artefact after it has been created. (@TODO Cfr Three Archetypes by Cyrille)

Example

Many business processes involve creating an artefact for later productive use. During an Ordering process, different users might add and remove line items of items they wish to order. They might change the quantities of the items they want. And an account manager might change the prices, change the quantities based on available stock, and so on. In the original design, the consumer listens to LineItemWasAdded, LineItemWasRemoved, QuantityWasChanged, PriceWasChanged, as well as events such as ShippingAddressWasAdded. The consumer, say an OrderFulfilment process, listens to most of these events, and projects a list of of items that need to be packed and shipped.

But OrderFulfilmentProcess doesn’t do anything until the OrderWasPlaced event arrives. That’s when it kicks into action. The OrderWasPlaced doesn’t have redundant information, so it mostly consists of an OrderId, and not much else.

We can now refactor the Ordering process, to keep all its events private. Instead, we make a single public event for OrderWasPlaced, but this one contains the complete information about the order. In fact, one way to look at it is that OrderWasPlaced is the actual order document. (@TODO I believe this is the Document Message pattern in Enterprise Integration Patterns, I need to look it up).

This is of course very familiar, precisely because this is how information travels in businesses, and has traveled for ages before computers: Someone working in the Order Fulfilment department is usually not aware of all the negotiations and changes that happen to a draft order. They only get a message with the resulting order, and act on that.

The point of the Summary Event pattern is to look for opportunities to apply it in domains that are not as mature as ordering, or in situations where it is not obvious that the two highly coupled systems could reduce the communication between them using this pattern.

Implementation

You can use a Projection to generate the Summary Event. A Projection is an event listener that persists state from events it listens to, and either exposes that state by responding to queries, or by emitting new events.

Weaknesses

  1. Sometimes the consumer does need to know a lot of internals of the producer to do its job. In that case, the Summary Event pattern is not helpful, as it would not only contain the resulting information, but also metadata about each step in the process.

  2. In the ordering example above, OrderFulfilment doesn’t care about the price of the order. Another consumer, say RevenueReporting, cares about the prices but not the exact items ordered. Yet they both receive the same OrderWasPlaced event with everything in there. There might be cases where this is a problem, for example if some of the information is sensitive.

  3. The new Summary Event adds yet another event type, a “new standard to replace all standards”.

  4. The Summary Event might not naturally belong to the domain of the producer. In that case, we can add an intermediate service, that consumes the producer’s events, summarizes them, and then emits the Summary Event for consumption.

Diagram

Original design:

Producer: OrderWasInitiated  LineItemWasAdded  PriceWasChanged ... OrderWasPlaced
                |                     |              |                   |
                v                     v              v                   v
Consumer: (update state)    (update state)   (update state) ...-> (start fulfilment)

OrderWasPlaced is refactored to a Summary Event, the other events become private:

Producer: OrderWasInitiated  LineItemWasAdded  PriceWasChanged ... OrderWasPlaced
                                                                         |
                                                                         v
Consumer:                                                        (start fulfilment)