Imagine we are tasked with implementing an API to check whether a discount count can be applied to an order. The Order domain object contains the items in the basket as well as the customer id:
class Order(
val items: List<Item>,
val customerId: CustomerId
)
We also have a domain object DiscountCode representing the discount count to be used.
There are several validation rules to check if the given discount count can be applied to the given order:
- Is the discount count expired?
- Are there items in the order that can not be discounted?
- Has this discount code already been used by someone else?
- (Is the customer allowed to use this discount code?)
For rules 1-3 we can say that they are clearly business logic and according to DDD belong in the DiscountCode aggregate:
class DiscountCode(
val id: DiscountCodeId,
val hasAlreadyBeenUsed: Boolean,
val startTime: LocalDateTime,
val endTime: LocalDateTime
) {
fun isApplicableToOrder(order: Order) {
return
startTime.isBefore(now) && endTime.isAfter(now) // rule 1
&& order.items.none(_.canNotBeDiscounted) // rule 2
&& !hasAlreadyBeenUsed // rule 3
}
}
We can easily load this DiscountCode from the database and then call the above function to check if it can be used with the given order without incurring any side effects.
The question is what to do with rule 4: Rule 4 can not be checked with just the DiscountCode class unless we embed a list of all allowed customers into the DiscountCode class, which is unfeasable if there are thousands of customers. Similarly, we can not embed a list of allowed discount codes into the Customer class because there may be just as many. In the database we can add a new table with valid customer + discount code tuples:
class DiscountCodeCustomerBinding(
val customerId: CustomerId,
val discountCodeId: DiscountCodeId
)
Thus, in order to check rule 4 we need to make another query to this database table.
Following DDD, where should this business logic for rule 1-3 and rule 4 live? We can not make a database query for rule 4 inside the DiscountCode class because it is a side effect. We could move rule 1-4 into a domain service that is allowed to make database queries but now we have created an anemic domain model. Putting rule 1-3 into the DiscountCode class and rule 4 into a separate domain service splits the logic into several places which is very error prone.