validationdomain-driven-designbusiness-logic

Placement of business logic as validation of data in architecture


I have a budget entity in my domain which can be closed by the user if the budget is open. This means if the budget opens today and closes in seven days, in between this time I can close it, not before (I'd have to delete it), not after (can't do any action).

This time constraint raised a doubt: should I add a validation in my domain code for that, like the one below, or should the validation be in the controller that will access a service which will access the domain?

class Budget {
  ...

  public close(): void {
    if (this.isOnPeriod()) {
      this.closed = true
    }
  }

  private isOnPeriod() {
    if (new Date() < this.closing_date) {
      throw new BudgetIsClosedError()
    }
    if (this.opening_date > new Date()) {
      throw new BudgetIsNotYetOpen()
    }

    return true
  }

  ...
}

I don't know how to explain it, but it this seem like this is business logic, although it also seems like this case (of not being on the period) will never even arrive to the domain because I am going to have validation for the data that arrives in the controller (but have in mind I don't know all the business rules and requirements yet).


Solution

  • This time constraint raised a doubt: should I add a validation in my domain code for that, like the one below, or should the validation be in the controller that will access a service which will access the domain?

    What you want most of the time is for all of the business policies to be expressed within the domain model, rather than scattered all over your code.

    The stuff outside of the domain model is not responsible for business policy, but is responsible to providing information to the domain model.

    So your example might look something like this:

      public close(Date today): void {
        if (this.isOnPeriod(today)) {
          this.closed = true
        }
    

    And somewhere else you would have code that looks like:

    Date today = new Date();
    
    Budget budget = someRepository.get(/* whatever */);
    budget.close(today);
    

    One additional advantage to this design: it is really easy to test that Budget::close handles all of its edge cases correctly because in the test you can pass whatever date you want.