In DDD, how do I bypass my business logic to reconstitute an aggregate in my repository?

When I instantiate an aggregate in my repository, I need to add a lot of dependent objects. (Aggregates are collections of dependent objects.) This creation / addition needs to happen regardless of business logic because this is existing data. I must always be able to load this existing data.

When my end user wants to manipulate those dependent objects, I need to enforce business logic to ensure my data is valid. Their attempt to add new data may fail.

How should I safely allow the repository to skip or defer the business logic checks while forcing my end users to adhere to the rules?


In my domain, I have an aggregate called Project that has a child aggregate called Budget and a collection of child objects called Disbursement.

public class Project {
   public Budget budget { get; }
   private List<Disbursement> Disbursements { get; }
   public void AddDisbursement (Disbursement newDisbursement) { }
}

A Budget has a collection of Sources and Uses. (A "source" is "where is the money coming from" and a "use" is "how will I be using this money".) Sources and uses have amounts.

public class Budget {
   public List<Source> Sources { get; }
   public List<Use> Uses { get; }
}
public class Source {
   public int SourceNo { get; }
   public decimal BudgetedAmount { get; set; }
   public decimal UsedAmount { get; } // Calculated
   public decimal AvailableAmount => BudgetedAmount - UsedAmount;
}
public class Use {
   public int UseNo { get; }
   public decimal BudgetedAmount { get; set; }
   public decimal UsedAmount { get; } // Calculated
   public decimal AvailableAmount => BudgetedAmount - UsedAmount;
}

This is how someone declares what they intend to do with the money. Then, of course, there's the actual way they use money, represented by the Disbursement collection.

public class Disbursement {
   public int SourceNo { get; set; } // This is where the money actually came from
   public int UseNo { get; set; } // This is what the money was used for
   public decimal Amount { get; set; } // This is how much actual money was used
}

The business rule is that a user can't add a Disbursement if the associated Source doesn't have enough available funds or if the associated Use has had too many funds allocated to it already.

public class Project {
   public void AddDisbursement (Disbursement newDisbursement) {
      var source = Budget.GetSource(newDisbursement.SourceNo);
      if (source.AvailableAmount < newDisbursement.Amount)
      { throw new Exception ("You don't have available funds.");

      var use = Budget.GetUse(newDisbursement.UseNo);
      if (use.AvailableAmount < newDisbursement.Amount)
      { throw new Exception ("You have overspend these funds.");

      Disbursements.Add(newDisbursement);
   }
}

This is simple and fine for governing what the end user tries to do. However, when I'm pulling an existing project out of the database, I need to add all of my existing disbursements at the same time, but I don't really care about enforcing the business logic there. It's existing data, so I must load it.

How do I allow my repository to pull in and instantiate the existing data without being subject to the business rules (which may not apply, anyway, until after the full set of data is loaded)?

I'm not using Entity Framework. Or, you could pretend that I'm using my own repository pattern on top of EF. (We looked at several ORMs and none met our requirements, so we ended up writing our own.) Bottom line: my repository must translate between the persisted data and the domain class. Which means this question could apply to factory patterns, as well. How do I safely have the system build a complex object while still having business validation to govern user actions?

Upvotes: 1

Views: 213

Answers (1)

VoiceOfUnreason
VoiceOfUnreason

Reputation: 57299

So the repository has to go through some sort of "AddDisbursement()" method, but the current method enforces business rules.

Right. So don't do that.

A FACTORY reconstituting an object will handle violation of an invariant differently. During creation of a new object, a FACTORY should simply balk when an invariant isn't met, but a more flexible response may be necessary in reconstitution. -- Eric Evans, Domain Driven Design...., Chapter 6.

Most commonly, during reconstitution we'll more directly use initializers to create our in memory data representations, rather than using methods designed to enforce policy.

So in a model like yours, we would normally see an Project initializer/constructor that allows the factory to provide a Budget and a list of Disbursements and use that, rather than using Project::AddDisbursement, to copy data into the object before releasing it for use.

Upvotes: 3

Related Questions