DDD How to Avoid Aggregate Root with 3 Level Children who has lots of invariants between each other, is this correct design or am i totally lost?

Viewed 43

I am new to Domain Driven Design, I have read Van Vaughn DDD book and I am trying to refactor my project according to DDD rules.

So here is my design for my aggregate roots and entities that I have designed but I have some concerns about it. please look at the below link for it.

My design

Basically my concern is about Offer aggregate root. It has OfferItems. In OfferItem entity I also have OfferItemScreen Entities.

Here are some notes that I have taken to find out invariants of Offer.

Offer

  • The Offer should have at least 1 OfferItem.
  • When you add service to an Offer, every net3 value should be recalculated for OfferItemScreen and this calculation should also reflect to OfferItem and Offer. (OfferItem net3 values should be modifed as sum of all OfferItemScreen net3 values. likewise Offer net3 value should have been modifed.)
  • When you change the mediaagencyid of the offer, all of OfferItemScreens of OfferItems net2 values should be recalculated from the discount percentage of the new MediaAgency.

OfferItem

  • OfferItem needs to have at least 1 OfferItemScreen.
  • OfferItem should have at least 1 date.
  • When you change the discount type (None,Percentage or NewPrice), OfferItemScreen Net1 values should be recalculated
  • When you update the OfferItem ShareOfVoice property then Offer ShareOfVoice property should be recalculated.

OfferItemScreen

  • When you update, OfferItem (grossprice,net1,net2,net3) and Offer(grossprice,net1,net2,net3) should be calculated again.

OfferItemDate

  • Dates can not intersect with the other dates in offeritem.

So I was kind of lost here. Since Offer is the aggregate root and it shouldn't expose OfferItem or OfferItemScreen to outer layers. So what i try is...

public class OfferItemDate : Entity
{
    internal OfferItemDate(DateTime beginDate, DateTime endDate)
    {
        CheckIfTimeRangeIsValid(beginDate, endDate);
        BeginDate = beginDate;
        EndDate = endDate;
    }
    public bool IsOverlap(OfferItemDate offerItemDate)
    {
        return offerItemDate.EndDate >= BeginDate && offerItemDate.BeginDate <= EndDate;
    }
    public bool IsOverlap(DateTime startDate, DateTime endDate)
    {
        return endDate >= BeginDate && startDate <= EndDate;
    }

    private void CheckIfTimeRangeIsValid(DateTime beginDate, DateTime endDate)
    {
        if (endDate < beginDate)
            throw new BusinessException("end date can not be less than begin date");
    }

}



    public class OfferItem : Entity
{
    internal OfferItem(string name,
        OfferItemType type,
        decimal shareOfVoice)
    {

        Check.NotNullOrEmpty(name, nameof(name));
        Check.NotNull(type, nameof(type));
        DoohclickCheck.MoreThanZeroAndLessThanOrEqualToHundred(shareOfVoice, nameof(shareOfVoice));

        Name = name;
        Type = type;
        ShareOfVoice = shareOfVoice;
        DiscountType = DiscountType.NoDiscount; //default value
        
        Dates = new List<OfferItemDate>();
        OfferItemScreens = new List<OfferItemScreen>();
    }
    
    public void AddScreen(Screen screen,Offer offer)
    {
        CheckNotScreenExist(screen.Id);
        var newScreen = new OfferItemScreen(screen, this);
        OfferItemScreens.Add(newScreen);
        UpdateGrossPrice();
        CalculateNetPricesForOfferItemScreens(offer);
    }
    public void AddDate(DateTime startDate, DateTime endDate)
    {
        var newDate = new OfferItemDate(startDate, endDate);
        CheckOverLapForDates(newDate);
        Dates.Add(newDate);
    }
    private void UpdateGrossPrice()
    {
        GrossPrice = OfferItemScreens.Sum(o => o.GrossPrice);
        switch (DiscountType)
        {
            case DiscountType.NewPrice:
                DiscountPercentage = Net1 / GrossPrice;
                break;
        }
    }
    internal void CalculateNetPricesForOfferItemScreens(Offer offer)
    {
        foreach (var offerItemScreen in OfferItemScreens)
        {
            CalculateNetPricesForOfferItemScreen(offerItemScreen.ScreenId,offer);
        }
    }
    internal void CalculateNetPricesForOfferItemScreen(int screenId, Offer offer)
    {
        var offerItemScreen = OfferItemScreens.FirstOrDefault(o => o.ScreenId == screenId);
        if (offerItemScreen == null)
            throw new BusinessException("screen do not exist.");

        offerItemScreen.CalculateNet1Price(this); //this method needs offeritem, it uses offeritem discount type.
        offerItemScreen.CalculateNetPricesAfterNet1(offer); //this method needs offer, it uses offer media agency discount

    }
    
}

here in Offer Aggregate Root

I have some trouble, here are couple of ideas that I couldn't find the solution

1st Try *should i take also dates??? this seems wrong, since dates should also be created from offeritem, and while creating an offeritem you can not pass dates then

 public class Offer : AggregateRoot
{
    internal void AddOfferItem(string name, OfferItemType type, decimal shareOfVoice, List<Screen> screens, List<OfferItemDate> dates)
    {
        var newOfferItem = new OfferItem(name, type, shareOfVoice);
        foreach (var screen in screens)
        {
            newOfferItem.AddScreen(screen, this);
        }
        foreach (var date in dates)
        {
            newOfferItem.AddDate(date.Key, date.Value);
        }


        OfferItems.Add(newOfferItem);
    }
}

2nd try **Id Problem over here: how am i gonna pass offerItemId since it is not created yet? **

    public class Offer : AggregateRoot
{
    //some code

    internal void AddOfferItem(string name,
            OfferItemType type,
            decimal shareOfVoice
            )
    {
        var newOfferItem = new OfferItem(name, type, shareOfVoice);

        OfferItems.Add(newOfferItem);
    }
    internal void AddOfferItemScreen(int offerItemId, Screen screen) 
    {

    }

    interval void AddDate(int offerItemId, DateTime startDate, DateTime endDate)
    {


    }

}

Third Try: this doesn't feel right since I return OfferItem from the method.

    public class Offer : AggregateRoot
{
    //some code

    //this doesn't feel right also since this is Offer agggregate and i return OfferItem Entity, 
    //OfferItems should remain internal and shouldn't leak to other layers.
    internal OfferItem AddOfferItem(string name,
            OfferItemType type,
            decimal shareOfVoice
            )
    {
        var newOfferItem = new OfferItem(name, type, shareOfVoice);
        OfferItems.Add(newOfferItem);
        return newOfferItem;

    }
    internal void AddOfferItemScreen(OfferItem offerItem, Screen screen)
    {

    }

    interval void AddDate(OfferItem offerItem, DateTime startDate, DateTime endDate)
    {


    }


}

And beyond "how should I implement what in where", I am still unsure about if OfferItems and OfferItemScreens should be entity. When I think about it I can not think of OfferItem without Offer and I can not think of OfferItem without Screens connected to it. So It seems like they should be one unit. But on the other hand that is gonna be a problem. The sales person can create an offeritem with 1000 screens sometimes. Not all the time, but in some cases that can happen. And now Offer Aggregate root is a very large cluster.

That's why sometimes I am leaning to make OfferItemScreen as Aggregate Root also but how am I gonna create the consistency in this case. Should I use eventual consistency? But these are at the same bounded context, and this is user input that comes from application service. So it seems right to have transactional consistency.

I am kind of stuck here and don't know how to continue, any help is appreciated. Thank you so much even for reading all of it.

0 Answers
Related