jsisodiya
jsisodiya

Reputation: 61

DDD: How to check invariant with multiple aggregate

I know that keeping large collections in Aggregates impacts performance.

In my use case i have STORE which can have multiple PRODUCT and each product can have CUSTOMIZATION(Not more than 10-20 customization).

I thought of creating one store aggregate only and update product and customization through it but as product collection can be large so it will impact performance. So I have two aggregates STORE(to create store) and PRODUCT(with storeId,all product operation) with this approach I am not able to check if product already exist or not.

what i am doing now is getting all products by StoreId in my handler and checking duplicate which is not right way as it should belong to my domain model.

Anyone has better idea to solve this. below are my domain models.

public class Store : Entity<Guid>, IAggregateRoot
    {
        private Store()
        {
            this.Products = new List<Product>();
        }
        private Store(string name, Address address) : base(System.Guid.NewGuid())
        {
            
            this.Name = name;
            this.Address = address;
        }
        private Store(string name, Address address, ContactInfo contact) : this(name, address)
        {
           
            this.Contact = contact;
        }
        public string Name { get; private set; }
        public Address Address { get; private set; }
        public ContactInfo Contact { get; private set; }
    }
 public class Product : Entity<Guid>, IAggregateRoot
    {
        private Product()
        {

        }
        private Product(Guid storeId, ProductInfo productInfo) : base(Guid.NewGuid())
        {
            
            this.ProductInfo = productInfo;
            this.StoreId = storeId;
            this.Customizations = new List<Customization>();
        }
        private Product(Guid storeId, ProductInfo productInfo, IEnumerable<Customization> customizations) : this(storeId, productInfo)
        {
            
            this.Customizations = customizations;
        }

        public ProductInfo ProductInfo { get; private set; }

        private List<Customization> _customizations;
        public IEnumerable<Customization> Customizations
        {
            get
            {
                return _customizations.AsReadOnly(); 
            }
            private set
            {
                _customizations = (List<Customization>)value;
            }
        }
        public Guid StoreId { get; private set; }

        public static Product Create(Guid storeId, ProductInfo productInfo)
        {
            return new Product(storeId, productInfo);
        }

        public void UpdateInfo(ProductInfo productInfo)
        {
            this.ProductInfo = productInfo;
        }

        public void AddCustomization(Customization customization)
        {
            
            this._customizations.Add(customization);
        }

        public void RemoveCustomization(Customization customization)
        {
          

            this._customizations.Remove(customization);
        }
    }

Upvotes: 3

Views: 1570

Answers (2)

Mehdi
Mehdi

Reputation: 339

Well as correctly Jonatan Dragon mentioned and you found the solution in an article of course you can use domain services but taking this approach for solving these kind of problems has the danger to fall in the anemic domain model pitfalls in future developments. This is the most common cause of loosing technical excellency in the domain layer. In general whenever a problem must be solved with objects collaborations, this kind of problems will be occurred. Therefore whenever is possible to avoid using domain services it's better to find the other answers that doesn't utilize this pattern. For your case the problem can be solved without using domain services by working around on some trade-offs to handle non-functional issues (like performance) and keeping the models rich and clean!

Let's consider some assumptions for designing aggregates to identify where do we want to involve trade-offs which we will accept for solving this problem:

1- In designing aggregates, just one aggregate's state must changes during one transactional use-case. (Greg Young)

2- In designing aggregates, the things can be shared among aggregates are only their IDs. (Eric Evans)

It seems these two assumptions make our minds enclosed in a frame that solve this kind of problems by only utilizing domain services. So let's look at them more deeply.

Many DDD practitioners and mentors like Nick Tune knows the transaction default scope over the entire BC in a use-case instead of only consider it for one aggregate. Therefor this is the place where we have some degrees of freedom to involve with trade-offs.

For number 2, the philosophy behind this assumption is to share only the part of aggregates that it's invariant and never modifies during the aggregate's lifespan. Therefor not so many aggregates get locked during a transaction in one use-case. Well if there's case that a shared state of aggregates changes on one transaction scope and there's no way for that to modify separately, technically there will be no problem in sharing it.

By mixing these two we can conclude to this answer for this problem:

You can let the Store aggregate to decide for creating a Product aggregate. In OOP words you can make The Store aggregate be the Product aggregate Factory.

public class Store : Entity<Guid>, IAggregateRoot
{
    private Store()
    {
        this.Products = new List<Product>();
    }
    private Store(string name, Address address) : base(System.Guid.NewGuid())
    {
        
        this.Name = name;
        this.Address = address;
    }
    private Store(string name, Address address, ContactInfo contact) : this(name, address)
    {
       
        this.Contact = contact;
    }

    public Product CreateProduct(Guid storeId, ProductInfo productInfo)
    {
        if(ProductInfos.Contains(productInfo))
        {
            throw new ProductExistsException(productInfo);
        }

        this.ProductInfos.Add(productInfo);

        return new Product(storeId, productInfo);
    }

    public string Name { get; private set; }
    public Address Address { get; private set; }
    public ContactInfo Contact { get; private set; }
    public List<ProductInfo> ProductInfos {get; private set;} = new();
}

In this solution i considered ProductInfo as a value object, hence checking duplication can easily be done by checking their equality. For ensuring the Product aggregate can not be constructed independently you can make it's ctor's access modifier as internal. Usually aggregate models placed in one assembly and ORMs can use non public ctors too, therefor this will create no problem.

There are some points to notice in this answer:

1- The Store aggregate must not use the internal parts of ProductInfo. With this approach ProductInfo can change freely as it's owner ship belongs to Product aggregate.

2- As ProductInfo is a value object, storing and recovering the Store aggregate is not a heavy operation and by converting techniques in ORMs this can reduce to storing and recovering data from only one field for ProductInfos collection.

3- The Store and the Product aggregates are only coupled for Product creation use-case. They can operate freely separate in other use-cases.

So with this approach you will achieve small aggregate separation in 99% of use-cases and the duplicate checking as the domain model invariant.

PS: This is the core idea of how to solve the problem. You can cook it with other patterns and techniques like Explicit State Pattern and Row Versions if it's required.

Upvotes: 2

Jonatan Dragon
Jonatan Dragon

Reputation: 4997

At first make sure it really impacts the performance. If you really need 2 aggregates, you can use a Domain Service to solve your problem. Check this article by Kamil Grzybek, section BC scope validation implementation.

public interface IProductUniquenessChecker
{
    bool IsUnique(Product product);
}

// Product constructor
public Product(Guid storeId, ProductInfo productInfo, IProductUniquenessChecker productUniquenessChecker)
{
    if (!productUniquenessChecker.IsUnique(this))
    {
        throw new BusinessRuleValidationException("Product already exists.");
    }

    ...
}

Upvotes: 1

Related Questions