I'm aware of the rules in aggregate relationships by Eric Evans. E.g., one aggregate root cannot hold references to another aggregate root's local entities (or can only do so transiently), a local entity within one aggregate root can hold reference to another aggregate root (etc, etc).I've come up with some questions regarding relationships/references between aggregates (and aggregates and repositories), which I may not have understood correctly. I'd love some (predictably illuminating) answers.Questions:1. Is it OK to pass repositories (for transient use only) to a function of an aggregate root?For example, if a Visitor::registerAsNewUser() function accepts a UserRepository, it can check for duplicate usernames rather than having the calling code (command handler/domain service) do so.
2. Is it OK to pass one aggregate root (again, for transient use only) to a function of another aggregate root?For example, if a Order::addLineItem() function accepts a Product aggregate, it can add the appropriate values rather than relying on the calling code to pull them out of the Product and pass them.I would assume the answers to 1 and 2 are "yes" because:* Aggregate root/repository parameters are only used transiently.* Aggregate roots/repositories are domain objects (the repository acting as a collection and abstracting the persistence mechanism).* Using aggregate roots and repositories as parameters enriches the domain model language.* Using aggregate roots and repositories as parameters allows you to move logic previously located in "domain services" into the domain models themselves (e.g., in the MVC approach, having "fat controllers" is considered bad because it's taking logic out of the model and putting it into the calling code).However, according to Eric Evan's rules on aggregate design, an aggregate should not be able to "reach outside of itself". Am I missing something here? Or does "reaching outside of itself" meaning holding a direct reference (rather than an identity reference)?
3. Is it OK to have bidirectional relationships between aggregate roots? What are the typical use-cases of such relationships?
4. If it is OK to have bidirectional relationships, then in creating the relationship you are actually modifying two aggregates at the same time (unless the relationship itself is an aggregate). Do bidirectional relationships violate the "commands should target one aggregate only" rule?
1. Is it OK to pass repositories (for transient use only) to a function of an aggregate root?For example, if a Visitor::registerAsNewUser() function accepts a UserRepository, it can check for duplicate usernames rather than having the calling code (command handler/domain service) do so.
BK> Infrastructure is intruding into the domain so you need a damn good reason.
In DDD i see little objection to this , you are coupling them but thats about it . I have also done this in CQRS to get a cheap transaction.
If there is a good reason yes .. however this can be tricky the rule no/ minimal infrastructure in the domain is far more important than a command modifying 2 aggregate and other guidelines.
On Tuesday, 12 January 2016 12:31:05 UTC+11, Bennie Kloosteman wrote:BK> Infrastructure is intruding into the domain so you need a damn good reason.I was under the impression that a Repository was part of the domain layer? E.g., you should be able to write your entire application so that it runs in memory (RAM) and that it is ignorant of the persistence mechanism (whether using event sourcing or ORM). At it's simplest level, a Repository can encapsulate a basic array.
I guess you could use a saga to make them "mutually aware of each other", but I'm not particularly fond of that approach. It's possible that most relationships can be unidirectional, as you are typically traversing in one direction in the write model (Order -> Customer::isPurchasingDisabled()) and either direction in the read model (reports of # orders per customer, for example).
Is it OK to pass repositories (for transient use only) to a function of an aggregate root?
Is it OK to pass one aggregate root (again, for transient use only) to a function of another aggregate root?
Is it OK to have bidirectional relationships between aggregate roots?
If it is OK to have bidirectional relationships, then in creating the relationship you are actually modifying two aggregates at the same time
Is it OK to pass repositories (for transient use only) to a function of an aggregate root?
I would think "no".
The one case where I'm still fuzzy is how one aggregate creates an instance of another. That factory has to come from somewhere, but I don't think it's the repository. I don't know that it isn't either.
Is it OK to pass one aggregate root (again, for transient use only) to a function of another aggregate root?
Again, I would guess no -- you can't query it, I don't *think* you should be sending it commands, so what are you going to do with it that you couldn't achieve with an id, and perhaps a version? I don't see how the aggregate changing its own state is going to be able to use stale data from another store to validate its own invariant.
class Order {
public function addLineItem(Product $product) {if ($product->isDiscontinued()) throw new DomainError("You can't purchase a discontinued item");}
}
class AddProductToOrderServiceCommandHandler {public function execute(AddProductToOrder $command) {$product = $this->productRepository->getProduct($command->productId);$order = $this->orderRepository->getOrder($command->orderId);if ($product->isDiscontinued()) throw new DomainError("You can't purchase a discontinued item.");$order->addLineItem($product);}}
You can actually record the event of creating the aggregate without actual creating it, and leave the "replay" to do the creation.
Why can't you query it for state?
class Order {
public function addLineItem(productId, availabilityService) {if (availabilityService.isDiscontinued(productId) throw new DomainError("You can't purchase a discontinued item");}
}
I was under the impression that a Repository was part of the domain layer? E.g., you should be able to write your entire application so that it runs in memory (RAM) and that it is ignorant of the persistence mechanism (whether using event sourcing or ORM). At it's simplest level, a Repository can encapsulate a basic array.
--
You received this message because you are subscribed to the Google Groups "DDD/CQRS" group.
To unsubscribe from this group and stop receiving emails from it, send an email to dddcqrs+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
That is the way I'd do it, but passing the ClassRepository and class ID to the Student object is exactly the same code, just located in a different place. If the rules of an aggregate need to coordinate with other aggregates, why not put that logic in the aggregate itself?
So in your example of EnrolStudentInClass, you have the command handler load the Class from the ClassRepository and the Student from the StudentRepository then pass the Class to the Student's enroll method?
That is the way I'd do it, but passing the ClassRepository and class ID to the Student object is exactly the same code, just located in a different place. If the rules of an aggregate need to coordinate with other aggregates, why not put that logic in the aggregate itself?
--
What about using partial class?
All files can have their proper name (I'd certainly have an Apply.cs), all put in a folder with the aggregate name.
Of course you can't (he said with unjustified bravado); it's the aggregate that you are creating that guards that state. Why should aggregate X even know which events are generated when aggregate Y is created? Why should that logic be shared across the different aggregates that might create Y?
Because I don't have access to it. I might do this:class Order {public function addLineItem(productId, availabilityService) {if (availabilityService.isDiscontinued(productId) throw new DomainError("You can't purchase a discontinued item");}}
But I'm not willing to pretend that Order.addLineItem() can make any assertions about the state of the Product aggregate in a transactionally meaningful way. Or put another way, product.isDiscontinued isn't within the Order aggregate boundary, and therefore can not be part of the invariant that the Order is supposed to enforce.
How is your example any different to mine? Perhaps I don't understand what this "availability service" is, especially given that a discontinued/availability status is on the product anyway? If a Product is an aggregate that contains an isDiscontinued property and Order is an aggregate that contains LineItems (which reference the product), and the rule is that discontinued products cannot be ordered, having this check on the Product parameter in the Order method is useful. Sure, the product could be discontinued by another user in the time between when the original thread loads the Product and when the adds it to the order, but this is an "eventually consistent" issue, and the same thing can happen in ACID databases.
interface Product extends Aggregate {
void discontinue();
boolean isDiscontinued();
}
interface Product extends Aggregate {
void discontinue();
}
interface ProductProjection extends Projection {
boolean isDiscontinued();
}
interface AvailabilityService extends Query {
boolean isDiscontinued(ProductId productId);
}
interface AvailabilityService extends Query {
boolean isDiscontinued(ProductId productId, Time asOf);
}
interface AvailabilityService extends Query {
boolean isDiscontinued(ProductId productId, Version version);
}I'm taking it a step further by having the repository enforces invariants between aggregates (in an eventually consistent rather than transactionally consistent manner).When you write single-threaded applications that operate solely in memory (not even writing anything to disk), then you realise that invariants are nested within one another, and the entire program can be transactionally consistent. You can have direct memory pointers between your aggregates. As soon as you want to persist your data to disk, you have a "distributed" system because you can't guarantee that an aggregate deserialised from disk is going to have the same memory address, and so you have to allocate unique IDs to each of your aggregates, so that an "in-memory" aggregate can reference an "on-disk" aggregate and vice-versa.
Are you sure that makes sense? And does it still make sense if most command succeed -- meaning that 2. and 3. could be re-ordered?
Exercise: work through the same flow when Client and Order are not aggregates, but just Entities within some common aggregate boundary.
For what it is worth, I think Order having an "eventually consistent" view of Product can make sense, but to me it means a different thing; we can send to Order.addItem(Product(Version(200)) command, and the Order can add the Product even though it can't see that version yet. Commands that need to see "the" state of another aggregate fail when the targeted version is still in the future. So Order.addItem would work, but Order.validate() or Order.accept() would not because the necessary data hasn't arrived yet.
Introducing the AvailabilityService just clarifies for me that the Order aggregate doesn't care about all of the Product.
interface Product extends Aggregate {
void discontinue();
}
interface ProductProjection extends Projection {
boolean isDiscontinued();
}
or even
interface AvailabilityService extends Query {
boolean isDiscontinued(ProductId productId, Time asOf);
}
when the Order is pinned to a particular version of the Product. I'm providing, for the context of the current command, a way for the Order to interrogate some past state of the Product, without constraining the implementation. Sure, the AvailabilityService can load the entire Product history if that's what makes sense; or load up to some specific event in the history of the product, or just hit the read model for a projection that is tuned for this use case, or....
Put another way, separating objects in the present from objects in the past allows helps clarify which data races matter. If I'm running a command on the order aggregate, real time changes to the Product don't affect the correctness of my actions at all.
Whoa whoa whoa -- both of those paragraphs ring alarms.
First, I can't align your comment about enforcing environments with any of the definitions of "eventually consistent" that I've run into.
When you add the context of a second aggregate to the mix (ie using the state of the Product in the command acting on the Order), what you are doing is analogous to adding (some) ProductEvents to the OrderCommand queue.
Especially since the Order command handler has no way of knowing that it has the more recent Product history.
- Client dispatches Product.reissue command
- Product accepts the reissue command
- Client dispatches the Order.addItem command
- Order rejects addItem because Product is discontinued ??
- Product_Reissued event becomes visible to Order command handler.
For what it is worth, I think Order having an "eventually consistent" view of Product can make sense, but to me it means a different thing; we can send to Order.addItem(Product(Version(200)) command, and the Order can add the Product even though it can't see that version yet. Commands that need to see "the" state of another aggregate fail when the targeted version is still in the future. So Order.addItem would work, but Order.validate() or Order.accept() would not because the necessary data hasn't arrived yet.
... In general I'd say the rule is that whenever an aggregate needs to modify itself in relation to the "current" state of another aggregate, there is a potential race condition. This you have to figure out how to handle on a case-by-case basis (automatically cancel the order in a saga/process manager, send an email to the user, notify a staff member to intervene, etc, etc). Whenever an aggregate needs to modify itself against a "particular" state of another aggregate, there is no race condition. The best way to do this is using some kind of service rather than the aggregate itself.
order.addProduct(ProductAtVersion(200));
However, I personally would be hesitant to decouple so much because at the extreme end you could end up having a "service" for each property of your aggregate.
you rebuild the aggregate by replaying the events, and when you replay the ProductDiscontinued event, you store the date on which the Product was discontinued (the event timestamp most likely)
the typical use case would be to prevent a user from ordering a product if it is currently discontinued.... I guess the term "current" is ambiguous,
The best way to do this is using some kind of service rather than the aggregate itself.
Of course, if the aggregate is going to be getting the product state from the command, it needs to pitch a fit if the data in the command isn't consistent with its own state -- ie if the productId doesn't match.
In this trivial example, you might note that the application, or the command handler, could notice that the product has been discontinued and refuse to validate the command. And you'd be right... but you can't do that without the business logic leaking out of the aggregate into the application layer. If we were dealing with more complicated business rules, that combined information about the product with information about the current state of the order, where should
If so, does tagging version numbers really solve the problem?
When you write single-threaded applications that operate solely in memory (not even writing anything to disk), then you realise that invariants are nested within one another, and the entire program can be transactionally consistent. You can have direct memory pointers between your aggregates. As soon as you want to persist your data to disk, you have a "distributed" system because you can't guarantee that an aggregate deserialised from disk is going to have the same memory address, and so you have to allocate unique IDs to each of your aggregates, so that an "in-memory" aggregate can reference an "on-disk" aggregate and vice-versa.
Because I don't have access to it. I might do this:class Order {public function addLineItem(productId, availabilityService) {if (availabilityService.isDiscontinued(productId) throw new DomainError("You can't purchase a discontinued item");}}
class Order {
public function addLineItem(productId, availabilityService) {if (availabilityService.isDiscontinued(productId) throw new DomainError("You can't purchase a discontinued item");
}
But I'm not willing to pretend that Order.addLineItem() can make any assertions about the state of the Product aggregate in a transactionally meaningful way. Or put another way, product.isDiscontinued isn't within the Order aggregate boundary, and therefore can not be part of the invariant that the Order is supposed to enforce.How is your example any different to mine? Perhaps I don't understand what this "availability service" is, especially given that a discontinued/availability status is on the product anyway?