1

In trying to learn DDD by applying it to a new project, I've run into an obstacle.

Here's a simplified structure of my very real domain: a project has many components which have many objectives which have many contracts for carrying out said objectives.

Project A
|_Component A
|_Component B
  |_Objective A
    |_Contract A
    |_Contract B

Rule: Each project and contract is allocated some amount of money. Invariant: The sum of all the contracts' funds in a project shouldn't exceed the project's funds.

This suggests that adding new contracts to an objective should be done through the project since it's the only entity that knows how much money it has. In DDD words, to enforce consistency, projects and objectives should be part of the same aggregate, with the project as its root.

However, intuitively, this doesn't make sense. It is objectives that contracts belong to, not projects. They exist first and foremost to help complete objectives and only indirectly projects. "Add a contract to an objective," sounds more natural than, "Add a contract to a project," too, and the code would reflect that: objective.add_contract(new_contract) vs project.add_contract_to_objective(objective, new_contract).

I'm also worried that this design might turn the project into a kind of god object.

Am I conflating aggregates with the has/belongs to relationship, or is my gut feeling onto something? Should I, perhaps, create a service—though I don't know what I'd call it, and I think that's a red flag—that copies the project's funds and passes it to the objective, like objective.add_contract(new_contract, project_funding)?

(On second thought, that sounds like a ticket to the anemic domain model I've heard so much about.)

3
  • What if objectives were assigned a budget? Commented Oct 2, 2023 at 11:55
  • @candied_orange I considered that, but from the domain perspective, it doesn't make sense. No one can predict exactly how much money each component or objective will need. The best they can do is ensure they don't go over as a whole. And, IMO, replicating the data unnecessarily only creates more complexity and potential for bugs. Commented Oct 2, 2023 at 11:58
  • That creates a first come first served situation. The first contract can consume an entire projects budget. There is no avoiding this. Every time you spend money under a budget you’re deciding how much will be left for other things. Commented Oct 2, 2023 at 12:02

2 Answers 2

3

You can model this more loosely coupled by creating two Aggregates and utilize Domain Events.

Project
    RemainingBudget
    
    OnContractAdded(Contract)
        RemainingBudget.Subtract(Contract.costs)
        RaiseDomainEvent(ProjectBudgetReducedEvent(ProjectId, RemainingBudget))

and

Objective   
    ProjectId
    RemainingProjectBudget  
    List<Contracts>

    OnProjectBudgetReduced(ProjectId, RemainingBudget)
        RemainingProjectBudget = RemainingBudget

    AddContract(Contract)
        if(Contract.Cost > RemainingProjectBudget)
            return Error
        Contracts.Add(Contract)
        RaiseDomainEvent(ContractAdded(Contract))

In the application layer, you will have to add some event handlers and a mechanism to dispatch events when saving changes to an aggregate. That adds a bit of complexity in that layer, but in return you will get a cleaner domain layer without the deep hierarchy.

-2

I would make sure a component and an objective have a reference to their parent. And if they are assigned to another parent, update the parent.

That way an objective can request to the component if it Allowed.

The component can do it’s own business rules plus asking the Project if it’s allowed.

Also when a contract or a component would be moved, they can check again if the new configuration is allowed.

public class Project
{
    List<Component> _Components = new List<Component>();
    
    
    public void Add(Component component)
    {
        _Components.Add( component );
        component.OnAddedToProject(this);
    }
    
    
    public IReadonlyList<Component> Components => _Components;
}


public class Component
{
    public Project Project { get; private set; }


    OnAddedToProject(Project project)
    {
        Project = project;
        
        if ( !IsValid )
            throw new Exception();
    }
    
    
    public void IsValid
    {
        get
        {
            if ( !Project.IsValid )
                return false;
                
            if ( !OtherConstraint )
                return false;
                
            return true;
        }       
    }
}
    
1
  • This answer seems to miss the point of the question, since it contains no word about DDD, aggregate roots and the OPs issues.
    – Doc Brown
    Commented Oct 2, 2023 at 18:34

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.