Patterns for Decoupling in Distributed Systems: Segregated Event Layers

Explicitly segregate a Bounded Context's events in visibility layers, with their own language.

By Mathias Verraes
Published on 11 May 2019



Segregated Event Layers

Explicitly segregate a Bounded Context’s events in visibility layers, with their own language.

Problem

The problem is the same as described in Explicit Public Events. Sometimes marking some events as public is not enough. You really need to be able to evolve the internal events separately, while designing the public events differently to suit the clients. The clients may need Summary Events or Fat Events, but you don’t want to muddy your Bounded Context with all these consumers’ needs.

There’s also the problem that if too many consumers impose their needs on a producer, and all depend on the same events, the consumers are implicitly logically coupled to each other.

Solution

Keep all internal events strictly private. Set up an adapter that listens to internal events, and emits a new stream of different events. This may be on stream for all public events, but we can go as far as setting up a separate adapter and stream for each consumer, highly adapted to their need.

The new event stream is effectively a different Bounded Context from the one we’re in. The stream has its own language, event types, and names. This makes the Segregated Event Layers effectively an implementation pattern to build an Anti-Corruption Layer. Some names may overlap or conflict with the names in the original Bounded Context, but this is fine: Having the freedom of defining concepts locally without trying to define them globally, is precisely why we use Bounded Contexts.

Example

In the private layer, we emit:

OrderWasInitiated {orderId}
LineItemWasAdded {orderId, productId, quantity}
OrderWasPlaced {orderId, customerId}
StockWasReserved {orderId, stockId}
TransportWasBooked {transportId, orderId}
OrderWasPacked {orderId}
OrderWasShipped {orderId} 

The adapter is a type of Projector that listens for these events, and emits a new stream for public consumption:

OrderWasPlaced {orderId, customerName, lineItems}
OrderWasConfirmed {orderId, customerName, lineItems}
OrderWasShipped {orderId, customerName, lineItems}

The public stream is clearly designed to suit the clients: OrderWasPlaced is a Summary Event, the others are Fat Events, and there’s no Completeness Guarantee because stockId and transportId are not in that stream.

The public stream is a different Bounded Context: Some of the language in the public stream does not exist in the private stream, like OrderWasConfirmed; and OrderWasPlaced and OrderWasShipped have a slightly different meaning from their equivalents in the private stream.

We could take this example further by making a separate adapter, stream, and language, for each consumer, such as a customer notification system, the sales department, the shipping company, etc.