I understand that Aggregates should be small and they should protect invariants. I also know that keeping large collections in Aggregates impacts performance.
I have a usecase, that needs to protect its invariants, but also will lead to large collection.
Aggregate is Vendor, and it can have multiple active Promotion(s). Each Promotion has PromotionType, StartDate and EndDate. The invariants are:
public Vendor : Aggregate {
public Guid Id;
public List<Promotion> Promotions;
// some other Vendor props here
public void AddPromotion(Promotion promo) {
// protect invariants (business rules) here:
// rule_1: if 2 promotions are already active during any time between promo.Start and promo.End then throw ex
// rule_2: if during any time between promo.Start and promo.End there is promo with same Type then throw ex
// if all is ok (invariants protected) then:
Promotions.Add(promo);
}
}
public Promotion : ValueObject {
public PromotionType Type; // enum CheapestItemForFree, FreeDelivery, Off10PercentOfTotalBill
public DateTime Start;
public DateTime End;
}
As we can see, Promotions
collection will grow while new promotions are added during time, and old promotions will get expired.
solution 1)
One possibility is to make Promotion
an aggregate on its own, containing VendorId, but in that case it would be difficult to protect mentioned invariants.
solution 2) Another possibility is to have a maintenance job that will move expired (EndDate passed) to some history table, but it's smelly solution IMO.
solution 3)
Yet another possibility is to also make Promotion
an aggregate on its own but protect the invariants in Domain Service, e.g.:
public class PromotionsDomainService {
public Promotion CreateNewVendorPromotion(Guid vendorId, DateTime start, DateTime end, PromotionType type) {
// protect invariants here:
// invariants broken -> throw ex
// invariants valid -> return new Promotion aggregate object
}
}
... but protecting it in PromotionsDomainService (and returning Aggregates) we risk race condition and inconsistency (unless we apply pessimistic lock).
What is recommended DDD approach in such case?
Your aggregate should contain only the data you need in order to fulfill its purpose. Reading the description of your problem, I don't see that Vendor needs the expired promotions for anything. Therefore you only need to keep the active promotions in the collection.
In your AddPromotion method, if there is an active promotion of that type, you will return an error. If there isn't any promotion of that type, you will add it and if there is an expired promotion of that type, you will replace it. Unless you have a huge number of promotion types (which doesn't seem to be the case), you will have a maximum of one promotion of each type. It seems that this will keep the collection to a very reasonable size. Let me know if this is not the case.
It is very possible that you need the expired promotions as historical data. But these should be on a read model designed for that purpose, not in the aggregate. For that, the aggregate could publish an event every type it accepts a new promotion and a listener would react to that event and insert a record in the historical promotions table.
Update:
After reading again the question, I realized that you don't even need to keep one promotion of each type. You will have a maximum of 2 promotions in the collection, so the size of the collection will be maximum 2, unless I'm misunderstanding it.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With