Why I Orient Events to their Producer

Why I Orient Events to their Producer

An Event shouldn't know who's listening

Context

Event-Driven Architecture is an evolution of a more generic Microservices Architecture. With Microservice Architecture, responsibility is bound within defined domain boundaries and each service takes full ownership of its domain. Ideally, no business operation would span across services and every task would fall neatly into a single microservices domain. Of course, the reality is more complicated and microservices must interact to accomplish any business operation. How they communicate becomes a paramount consideration and is exactly what Event-Driven Architecture addresses.

Working Example

Consider the simple e-commerce example; Our company Widgets Inc. has a backend composed of an Order Service, Inventory Service and Email Service. When a Customer places an Order the Order Service must record the details of the Order, the Inventory Service needs to reflect the change in inventory and the Email Service must notify the Customer of their Order status. Fundamentally, the operations necessary from each service are well defined by the business; it's the degree of coupling among them we can control.

Communicating

Great communication yields a resilient system and great communication is all about the degree of Coupling. When microservices work together they will need to communicate with one another. As microservices evolve independently from one another, communication between them requires deeper decoupling.

Tight and Dependant - Synchronous

In a synchronous approach, services directly call http endpoints in a request/response model. This tightly couples the system together and can lead to cascading failure. Further, this approach can yield a distributed monolith, brittle and difficult to operate, all the benefits of microservices are lost.

Ball Room Dancing - Orchestration

A slightly more asynchronous approach is to Orchestrate or Choreograph services within a workflow. In this approach, it's common to introduce an Orchestrator. An Orchestrator is responsible for coordinating the workflow and, if necessary, executing compensation upon failure. Here the services are only coupled to the Orchestrator while the Orchestrator is coupled to all participating services. This variation of tight coupling is due to treating this business operation as a Distributed Transaction.

I'll place Distributed Transactions, Orchestration vs Choreography out of the scope of this post but each yield varying tradeoffs.

Consider the Events

In both our Synchronous and Asynchronous variations, our Events can be viewed as commands with implicit knowledge of the Workflow and the Consumer of the command.

Bad Messages

For example, this might be our associated Message Schemas:

message PlaceOrder {
  string MessageId = 1;
  string ProductId = 2;
  int32 Count = 3;
}

message UpdateInventory {
  string MessageId = 1;
  string ProductId = 2;
  int32 Count = 3;
}

message NotifyCustomer {
  string MessageId = 1;
  string CustomerId = 2;
  string Status = 3;
}

Each Message is imperative and directs a Consumer to take action. Whether these messages are communicated Synchronously or Asynchronously is irrelevant. Within these messages lies the knowledge of What & When to execute particular functions. This becomes a brittle distributed mess because each service must be oriented to execute within a predefined workflow there's no room for changes or discrete failure.

Commands by their very nature mean the command source knows what must be done next. Whether it's an Orchestrator or the services are Choreographed, commanding one another spreads Knowledge where it shouldn't be. For example, within NotifyCustomer there's a string representing Status but exactly which domain understands Status?

Better Approach

What we must recognize is that Producers are discrete domains and we shouldn't muddy those separations. Events should reflect the perspective of the originating domain. This means that a single domain can only speak for itself. We won't let the Order Service command the Inventory Service. Neither will we need an Orchestrator to command our domains.

message PlaceOrder {
  string MessageId = 1;
  string ProductId = 2;
  int32 Count = 3;
}

message OrderPlaced {
  string MessageId = 1;
  string CausationId = 2;
  string ProductId = 3;
  int32 Count = 4;
}

message InventoryUpdated {
  string MessageId = 1;
  string CausationId = 2;
  string ProductId = 3;
  int32 Count = 4;
}

Now we speak in the past tense after the Order is placed. Each domain simply states its domain knowledge. The Order Service can only say the Order is placed, it cannot tell anyone to update their inventory. It's the responsibility of the Inventory Service to know what to do when an Order is placed. Finally, our Email Service knows that the Order is placed and inventory is updated indicating a predefined Customer notification.

More than simply a semantic argument, the underlying intention is to maintain domain integrity and service independence. The common contrary argument is that the state is more difficult to ascertain with this approach. Certainly a valid perspective, however, the state of any system is simply the aggregate of Events and can be known precisely via Message & Causation Ids.

TL;DR

Events should reflect the perspective of the Producer and not implicitly dictate the Events' Consumption.