Microservices – Passing Information from One Service to Another with CQRS

cqrsmicroservicesread-model

We're building two microservices that use CQRS and Event Sourcing: an Order Service and an Inventory Service. When an order is made, the inventory of that item must be decreased by the amount ordered. An order can also be canceled. In that case, the inventory must be increased again.

So in the Order Service we store an OrderCreated event that contains the necessary details for the Inventory Service: a list of Item instances with their IDs and amounts. Sample code:

public class OrderCreated
{
    public int OrderId { get;set; }
    public IList<Item> Items { get; set; }
}

public class Item
{
    public int Id { get; set; }
    public int Amount { get; set; }
}

This event is also sent to the Inventory Service (could be mapped to a shared contract), so the Inventory Service can decrease the stock for the items.

When an order is canceled, we store an OrderCanceled event. For the Order Service, this only needs to contain an OrderId, eg:

public class OrderCanceled
{
    public int OrderId { get;set; }
}

But how do we now send the items to the Inventory Service?

Option 1

Add the items to the OrderCanceled event:

public class OrderCanceled
{
    public int OrderId { get;set; }
    public IList<Item> Items { get; set; }
}

This seems strange to us, because when re-applying the event (ie hydrating), this serves no purpose.

Option 2

Create an Event Handler for the OrderCanceled event, that retrieves the aggregate and accesses items, eg (not-real-life-code):

public class MyEventHandler : IEventHandler<OrderCanceled>
{
    public void Handle(OrderCanceled e)
    {
        var order = _repository.Get(e.OrderId);
        var message = new OrderCanceledMessage
        {
            OrderId = e.OrderId,
            Items = order.Items.Select(x => new ItemMessage(...))
        };

        _messageSender.Send(message);
    }
}

The weird thing here is that we're retrieving the Order instance, even though we already retrieved it in our command handler previously (i.e. when the user canceled the order, a command was sent to the system). So we retrieved our aggregate twice.

Option 3

The same as option 2, but instead of retrieving the Order in the Event Handler, we let the Event Handler call the read model to retrieve the items.

But in most articles I read, it is advised not to have the command side call the query side (though often with little argumentation).

Option 4

Send a message with only the OrderId but have the Inventory Service listen to all necessary messages of the Order Service and build its own read model of the orders.

This means we would actually have components create a read model for their own use, instead of read models for others. In the above example, the Inventory Service would build a model of the orders with its items.

The advantage seems here that you can build a read model purely for your own use. In a situation of microservices, we fear that a component might have to build too many different read models for all the different microservices.

A disadvantage could be that changes/additions of events in the Order Service would require changes in the read model generation of the Inventory Service.

So what am I asking here?

  • How do I tackle the issue where an Event Handler needs more information?
  • Does a microservices architecture mean I will need lots of read models (aka endpoints) per client?

Best Answer

We're building two microservices that use CQRS and Event Sourcing: an Order Service and an Inventory Service. When an order is made, the inventory of that item must be decreased by the amount ordered. An order can also be canceled. In that case, the inventory must be increased again.

You may be getting yourself tripped up because you are conflating manipulation of the order with reservation of the inventory.

Hint: creating an order doesn't deplete your stock; shipping product depletes stock.

Sometimes the patterns don't fit because they are trying to tell you that the model is broken.

Send a message with only the OrderId but have the Inventory Service listen to all necessary messages of the Order Service and build its own read model of the orders.

I want to call this out explicitly as a bad idea -- the inventory service should not be trying to build its own read model of orders; that couples the inventory service to the internals of the order model, and means that you can't easily change the order model without also re-deploying the inventory service.

But... it's perfectly reasonable for the inventory service to have its own model for "reserved inventory" that it builds up because of the messages that it receives from the order service.

In other words, during cancellation the inventory service doesn't care what the order items are, it just cares what stock was reserved for that order. It can look in its own store for that....

How do I tackle the issue where an Event Handler needs more information?

One thing to keep in mind: by the time an event reaches an event handler, the event is stale.

If the Inventory Service receives an event, and needs additional information to act upon it, then it queries the authority for the information it needs. So if the inventory service needs to know the state of the order because the Order was cancelled, then it sends a query to the order service asking for the information that it needs.

Note the separation - you establish an api contract that allows the inventory service to ask for what it needs, and a schema for the messages exchanged. The two services can independently change in any way that respects that API.

So you might imagine an exchange where the inventory service gets a thin notification that an order was cancelled, and then queries the order service to get a fatter representation of the order at that time.

That query doesn't necessarily need to be synchronous -- the result of the query is just another input message to the inventory service.

Does a microservices architecture mean I will need lots of read models (aka endpoints) per client?

Write only databases aren't very interesting, so you are going to need endpoints that let you get the data back out. If you are careful about your api designs, you won't necessarily need more read models than you would in a monolith. But you will sometimes need to support more than one read model for a given capability, so that legacy and upgraded clients can both access the information that the need.

Related Topic