Domain Driven Design (DDD) for microservices data architecture
DDD is a widely discussed and implemented pattern for microservices. Microservices interact with domain models to save state of business. Domain models are data models that are domain specific (specific to each microservice). For example,
- authorization microservice may have a domain model with user_id and other auth details of user
- order microservice may have a domain model with customer_id and other customer specific details
- shipping microservice may have a domain model with customer_id, address etc
All three are instances of customer business object, but applied in different context. This essentially duplicates data across microservices, and that’s ok per DDD. What we have done is establish a Bounded Context for data per microservice. In the context of authentication microservice, the boundary of Customer is defined to be user credentials. In another context, Customer has a different boundary.
A Bounded Context of data can serve more than one microservice if they all depend on the same BC. A logical “business microservice” (to borrow from Microsoft .NET terminology) may be implemented as 2 or more physical microservices.
Another feature of DDD is that the data and the microservice dealing with it should be cohesive and self sufficient. There is no other data it should need from outside, and there is no other data it updates outside the BC. These domain data models can sit on any type database out there.
Data propagation beyond Bounded Context in DDD
There might be instances where data propagation across domain models is needed (circumventing the idea of Bounded Context), for example to retire a customer or to make an update. This is a challenge and is usually solved using eventual consistency.
Eventual Consistency Models
Changes can be propagated across bounded contexts through AMQP requests (using messaging tools like RabbitMQ) which is asynchronous (non-blocking) or through HTTP requests that calls out to another microservice to make the update (which is synchronous and blocking — waiting for the response to come back). Non-blocking nature of messaging protocols makes it a recommended change propagation method.
Daisy chaining HTTP requests between microservices is an anti-pattern (not recommended) because if you do that, you have an application with SOA but no microservices granularity. For example, when shipping service issues an HTTP request to order service, then order issues an HTTP to authorization service.. and so on).
So if you happen to use HTTP instead of AMQP for change propagation across domain models, daisy chaining is a bad idea. Especially if the HTTP request is issued as a result of another HTTP request from the front-end where the client is waiting for response. It’s best to leave an asynchronous message and have the MQ’s pub-sub or event-driven model deal with it, while your service responds back to the client’s HTTP request.
Repository — Aggregate pattern
Domain models are not persistence layers or data infrastructure layers. They maintain state for applications but does not persist the overall business logic. Business logic is usually persisted in a normalized relational database that microservices usually don’t have to interact with continuously. Whereas domain models stay in memory in ORMs (Object Relational Mappers), or they stay in SQL or NoSQL databases.
A repository pattern uses a service (that you call repository) to do CRUD operations on persistent data. Any microservice that wants to persist data uses the repository to do so.
The best practice is for repository to use aggregates for insert/update/delete operations. Aggregate implies representation of related data elements in a wholesome domain of its own. Classic example is order domain.
You cannot insert an OrderItem as if it’s a value object because it has strong relationship to Order entity and also has its own identity. Similarly you cannot delete an Order without thinking of the implications of losing the child OrderItem. Doing so will violate the integrity of Order aggregate.
Address similarly has a strong relationship to Customer (not shown in pic) but in the context of Order aggregate Address is a value object. Order_id from Order table would be the aggregate root for Orders.
The ideal design is to use one repository per aggregate making sure insert/update/delete operations are done as atomic transactions that doesn’t violate the integrity of aggregate. This approach leads to a consistent mechanism to maintain reference data in the face of complex microservices.
Reference:
Microsoft’s documentation:
https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice
Martin Fowler:
https://martinfowler.com/tags/domain%20driven%20design.html