I am kindly asking for your guidance on where the logic connecting various entities and aggregate roots should reside. In our upcoming project, the code architecture will combine Domain-Driven Design (DDD), Clean Architecture (CA), Command Query Separation (CQS), and use-cases based on REST. Despite reading two books on these topics, numerous articles, and even consulting with Chat-GPT, there is still one aspect we don't fully understand: where to place the logic that connects multiple aggregate roots and domains in an ERP system.
I've considered many possibilities, but none seem satisfactory.
From my understanding:
- DDD is primarily a design approach, emphasizing that all logic should originate from the aggregate root.
- CQS involves separating commands and queries.
- Use-case driven architecture involves calling commands or queries to handle logic or data retrieval.
- Clean Architecture divides the system into layers to avoid mixing domain logic with persistence, presentation, and application layers. Crucially, the domain should not be aware of higher layers.
Could you please explain where the logic that connects multiple aggregate roots and domains should be placed?
Let me present the best workflow I've found so far. Let's take the Order Management domain as an example. There are three entities: Order, OrderLine, and OrderBatch. The domain layer will contain logic to ensure data correctness. Each entity will have value objects and methods for validating aspects such as quantity or stock, ensuring they aren't too low, and the SKU number. The persistence layer, in this case, is Entity Framework Core, which handles mapping to the RDBMS. We wont use the repository pattern (an extra abstraction layer is unnecessary, we won't be changing the ORM). So far, so good.
Now, let's discuss the challenges. We have a use case called "UpdateOrderBatch," which updates the quantity ordered in a particular batch. The controller will call the use case, which will then use... well what exactly?
Here is some pseudo-code:
USECASE UpdateOrderBatchSELECT data for the entire aggregate root of the order (Orders, OrderLines, OrderBatches)
- Here, I don't know what to do with related domains like Inventory, Users, etc. I think they should also be selected.This approach certainly won't be efficient.
SELECT all related aggregate rootsThe entire business logic validation lives here (like checking if stock can be decreased)
- I don't think it's desirable to split this into services because I doubt it will be reused in other use cases. Additionally, it spans multiple domains. Unless the logic for whether the quantity on OrderBatch can be changed should live inside the Order service, which would then handle multiple domains like Inventory, etc.
CALL reusable service methods (InventoryManagementService.DecreaseBatchStock, OrderService.SaveOrder, etc.)
- These services will operate only on aggregate roots.