0

I'm trying to follow the database per read/write per microservice pattner with HTTP CQRS API.

following this example

Asset Microservice

Write Model:

  • AssetWriteDb (mssql)
  • Asset class/table
public class Asset
{
 public Guid AssetId {get;set;}
 public Guid ContractId {get;set;} //reference to contract of other microservice
 ...
}

Read Model

  • AssetReadDb (mongodb)
  • AssetAggregate class/collection
public class AssetAggregate
{
 public Guid AssetId {get;set;}
 public Guid ContractId {get;get;}
 public string ContractNumber {get;set;} //this comes from Contract Microservice
 ...
}

Contract Microservice

Write Model

  • ContractWriteDb (mssql)
  • Contract class/table
public class Contract
{
 public Guid ContractId {get;set;}
 public string ContractNumber {get;set;}
 ...
}

Read Model

  • ContractReadDb (mongodb)
  • ContractAggregate class/collection
public class ContractAggregate
{
 public Guid ContractId {get;set;}
 public string ContractNumber {get;set;}
 public int AssetCount {get;set;} //this comes from Asset microservice
 ...
}

Contract aggregate syncronization eventHandler example:

public class ContractAggregateHandler :
 IHandleMessage<ContractChangedEvent> // published from ContractWriteDb mssql repository
 IHandleMessage<AssetChangedEvent> // published from AssetWriteDb mssql repository 
{
 
 public async Task Handle(ContractChangedEvent message)
 {
   await _bus.Send(new RefreshContractAggregateCommand(message.ContractId));
 }

 public async Task Handle(AssetChangedEvent message)
 {
   //since the event contains only AssetId, I need to retrieve the data from the asset microservice. i have two options to obtain the contractId from asset microservice:

   //call the AssetApi microservice reading the AssetAggregate collection (mongodb)
   //var contractId = await _mediator.Send(new GetAssetContractIdQuery(message.AssetId);
   
   //call the AssetApi microservice reading the Asset table (sqlserver)
   //var contractId = await _mediator.Send(new GetAssetContractIdFromWriteDbQuery(message.AssetId);


   await _bus.Send(new RefreshContractAggregateCommand(contractId));
 }
}

Following the rule that Queries should always query the Read Model and Commands should always read and write the Write Model, what are the best practices to achive this?

in the first case (reading the mongodb asset read model), I think it's wrong: the AssetChanged event comes from the AssetWriteDb (sql server) and querying the read model is not safe. also, if I base the aggregates generation by other aggregates, I should listen the AssetAggregateRefreshedEvent, but this will create infinites loops between aggregates generation because the AssetAggregates will need to liaste the ContractAggregateRefreshedEvent that these operations will never ends.

in the second case, (reading the sql asset write model) I think it is the safest but I need to manage a lot of queries that are "wrong" because they are not following the rule "queries must get data from read model". That's why to avoid mistakes, I need to differentiate them using a different ending word like "FromWriteDbQuery"

there is a third option that I obviously didn't want to evaluate: directly querying the AssetWriteDb from the Contract Microservice

NOTE: there is a "public" api gateway that "protects" all internal microservices from the external. the api gataway is exposing always right queries needed for clients that are querying mongodb in the right way. this question is just about internal processing of the aggregates and how to "query" Write Models between microservices

NOTE 2: I didn't write the "synchronization" business logic (the RefreshContractAggreagteHandler) because it's simply a "sql query" to the ContractWriteDb projecting a "ContractAggregate", then to map the assetCount I have the same issue, I want to query the AssetWriteDb from the Contract Microservice, so there is exactly the same of the main question)

pfab.io
  • 273
  • 1
  • 2
  • 11

1 Answers1

0

Remember that there can be arbitrarily many read models, optimized for the particular query/queries. Assuming that the changes to the contract ID in the asset microservice are captured in an AssetChangedEvent, then your contract service can maintain its own mapping of assets to contract IDs as part of its write model and then when you need to resolve the contract ID for a given asset, you check that mapping. Even though that mapping is concretely part of the contract write model (it might be a table in the contract mssql), conceptually it's a read model for assets (in fact the stream of AssetChangedEvents is a read model for assets, which means the event handler already has at least a toe in the asset read model pool as well as in the contract write model pool).

The AssetChangedEvent handler then checks first to see if the event is touching the contract ID; if so, it updates the mapping.

As an aside, it looks like this microservice decomposition was based on taking the tables from a relational schema and making each table its own microservice. This is one of the expressways to a lot of extra complexity (especially if you want synchronization). It's generally a better idea (though one that nearly every practitioner has to learn "the hard way") to decompose based on the commands. If assets and contracts are so tied together that changes to one tend to ripple into changes to the other (especially if stronger consistency guarantees are required), then having them in the same microservice might be the best idea.

Levi Ramsey
  • 18,884
  • 1
  • 16
  • 30
  • about decomposition i don't want to change the topic: i just gave an example. if you want we can talk about CustomerMicroService and OrderMicroService. Customer aggregate could have OrderCount. it's the same thing. – pfab.io Jul 31 '22 at 20:10
  • I don't understand the point of having a "read model" of the assets inside the write model of the Contracts. Honestly I think that Write models should have only data that they own. But, even if we try this approach, we have the same issue: synchronization, getting "write model data" from another microservice. About the AssetChangedEvents: it contains only an AssetId, not the data of the asset. The handler doesn't have anything except an AssetId, and it needs to query the asset "write model" microservice to get something the the business requirements need on the ContractAggregate read model – pfab.io Jul 31 '22 at 20:15
  • Yes, Customer and Order service will exhibit the same problem because thinking about microservices in terms of data rather than actions is what starts one on the path to painting themselves into a corner. – Levi Ramsey Jul 31 '22 at 22:55
  • I admit I was assuming `AssetChangedEvent` would be a richer domain event. As it stands, since it's only meaningful to a subscriber which can read the Assets mssql, it shouldn't be exposed to anything that doesn't read that mssql DB. You can have the process which projects assets as they update into mongo publish an analogous event (essentially saying "to whom it may concern, this asset has been updated and you can see it") and then the subscriber can check the asset read model. – Levi Ramsey Jul 31 '22 at 22:59
  • Sorry I was not clear about what AssetChangedEvent contains because i though it was implicit by my question: I need to retrieve data from WriteDb, if I had data in the event, i didn't need to query anything. I'll try to be more specific in the question. The point of having data in the event, is that, in the moment that the Event is handled by the handler, the data could not be updated anymore. I though that it is better to have always the latest version of the data when you are processing an aggregate (or simply you are processing a Command) this is why I need to read a write db – pfab.io Aug 01 '22 at 07:39
  • Like I said in the main question, subscribing the event of the mongo aggregated "refreshed" can't work, because this will create an endless loop if you need to do the opposite in the contractAggregate handler – pfab.io Aug 01 '22 at 07:40
  • There's an easy fix for the infinite loop on the mongo refresh event: if the update wouldn't/doesn't change what's in mongo, then don't emit a refresh event. – Levi Ramsey Aug 01 '22 at 17:01