0

I'm learning about DDD and have come up with a strange situation which I'm not entirely sure how to handle. It is my understanding that an aggregate, or an entity with an aggregate, can have a reference to another aggregate root. I have the following scenario, where I want to model a very basic e-commerce website. ShoppingCart and Product are their own aggregate roots

class ShoppingCart {
  public id: int
  public maxRemainingPrice: number (will be VO in future)
  public List<Product> products

  addProduct(p: Product) {
    if (maxRemainingPrice - p.price > 0) {
        maxRemainingPrice -= p.price 
        products.append(p)
    }
  }
}

class Product {
   public id: number
   public price: number (will be V0 in future)

   public setPrice(price: number) {
      this.price = price
   }
}

Now lets say we want to update the price of a Product. This might invalidate the invariant in multiple shopping carts where there is a maximum spending limit. My questions are

  1. what principal of DDD does this model not follow? I imagine that it has something to do with encapsulating all invariants inside of one aggregate root, so that changes to a Product must go through a shopping cart. But this feels quite backwards.

Lets say I introduce a new entity which is apart of the ShoppingCart, named CartItem

class CartItem {
    public id: number
    public price: number (will be VO in future)
}

Now lets say that I update ShoppingCart like so

class ShoppingCart {
  public id: int
  public maxRemainingPrice: number (will be VO in future)
  public List<CartItem> items

  addProduct(p: Product) {
    if (maxRemainingPrice - p.price > 0) {
        maxRemainingPrice -= p.price 
        items.append(new CartItem(p.id, p.price)
    }
  }
}

This way now the prices are tied to the time that a product is added to a cart, and no changes to a Product can effect the shopping Cart's invariants of price limits. But lets say the business imposes an invariant that "if a product's price is changed, then it must be removed from all shopping carts"

  1. How would I "remove" an item from a cart if the price changes? It seems like I would have to have a transaction spanning both of these aggregates, Price and ShoppingCart. I really am looking for any solution to changing the model that can mitigate this!

1 Answer 1

0

what principal of DDD does this model not follow? DDD is not a principle

.its an approach.

what i understand about your problem i suggest you before decision consider that you want to remove Item of your choice or customer choice. since its ecommerce removing item of your choice is not appropriate in general(i dont know specific concern of you business).

  • first scenario that you choose what to remove: after product changed in price you may publish a domain event of that(in distributed integration event) which triggers a handler in application service (eg: Mediator). the handler will load the cart aggregate and perform remove operation based on you business formula.
  • second scenario is more business flow concern. i may think of that you wont change or remove cart item however you maybe want show the total price exceeded in UI to inform. and on check-out operation when your cart move to Order you can stop or throw error or not allow it and inform customer to update the items so meet the limitation.

Note: also you can skip domain event and do the steps in application service.

I imagine that it has something to do with encapsulating all invariants inside of one aggregate root

first if the invariant checking is within the aggregate scope yes but i suggest to use ValueObjects . we use ValueObjects on every Aggregate root and Entity properties which helps to easily encapsulate validation. an example :

public record BankAccountNumber : IValueObject
{
    private const string Pattern = @"^[0-9-_\\.\\/ ]{0,20}$";

    public string Value { get;}

    private BankAccountNumber(string value)
    {
        Value = value;
    }

    public static BankAccountNumber CreateInstance(string accountNumber)
    {
        InvalidAccountNumberException.TrowIfNotMatchPattern(accountNumber,Pattern);
        return new BankAccountNumber(accountNumber);
    }


    
    public override string ToString() => Value;
}

if the elements of checking or validation etc is not only in scope of your aggregate and also consider another one from another aggregate you may follow the grasp principle(pure fabrication) in OOD that in this situation lets you to define another object which in Clean Architecture and DDD you translate it as domain service. you may have domain service that get value from Aggregate root A and Aggregate root B and do the job(could be any thing). caution : you wont check business rules in Application Layer Service .That is only for coordination . you load both aggregate roots and call Domain service which is placed in domain service and pass params to it. application does not have knowledge just coordination.

And for last thing, i think you have to think if maxRemainingPrice is belong to Cart or Not. How it will be set? it looks more regulation from regulatory rather than a property in Cart. i am uncertain about it since it may have definition in your business domain so you have to know about it, just consider it.

Sign up to request clarification or add additional context in comments.

5 Comments

Again though, these solutions really are showing eventual consistency tactics. One thought I had was to simply ensure that people’s carts dont have the product in it before changing price, so we would first remove all instances of the cartItem, then update the product’s price. No invariants are violated
i disagreed about these solutions really are showing eventual consistency . you can publish domain event eg: using mediator pattern within modular monolithic OR publish integration event between services using eg: Kafka in microservice. the second one is eventual consistent but the first is consistent.
does the first solution look something like having a domain service coordinate two aggregates? If so that would imply both aggregates are stored on the same database for transactional consistency. But transactional consistency should only stay within a singular aggregate, which is why I want to redesign the model to reflect this rather than have a domain event
transactional consistency and transactional boundary are different concepts but may related. an aggregate has its transactional boundary. i disagreed "transactional consistency should only stay within a singular aggregate" since the correct one is transactional boundary . you may have a process which reflects on two aggregates and all should be persist successfully or not at all in failure(Unit of work). there is no problem with it. each aggregates has its own transactional boundary and manipulated in a atomic execution on each steps then commit the all changes.
and please consider that application service is the place to coordinate NOT a domain service. the domain service just do the business like calculate something not persist or any technical or low level. it is like your application service load aggregateA and aggregateB from db and then pass require param to a domain service call where it returns some result and then update aggregateA and then update aggregateB and then commit changes. you may need or not need it as i mentioned refer to grasp pure fabrication.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.