DDD Discussion: Business logic validation

Jyoti
4 min readOct 22, 2023

Domain Driven Design, a software design approach where we model or design application closely related to business domain with input of domain experts. In DDD, we try to put the logic related to domain in domain layer saying only domain should know what it is, how to validate itself and any it will manage any operation related to domain.

Domain, in software development is generally discussed and come from discussion with business people. The term “domain” refers to a specific problem area or subject matter for which the software is being created. It typically emerges from discussions with domain experts, such as business stakeholders.

Regarding where to put business logic validation in a DDD application:

Application Layer:

The application layer, which often includes application services, serves as a bridge between various application components. It handles tasks like passing API requests from controllers to repositories, third-party API calls, and application-level requirements such as authentication and authorization. If the validation primarily concerns overall request consistency or application-level requirements, it’s best placed in an application service.

Domain Layer:

Business validation logic should primarily be placed within the domain layer of your application. The domain layer is responsible for modeling the core business concepts, rules, and behaviors. Here are some common places within the domain layer to put business validation logic:

  • Entities: Business validation logic can be encapsulated within domain entities, which represent core business objects. For example, validation rules for product prices and quantities can be placed within a “Product” entity. Here is an example:
class Product {
val price: Double
val quantity: Int

fun validate() {
if (price < 0 || quantity < 0) {
throw BusinessValidationException("Price and quantity must be non-negative.")
}
}
}
  • Value Objects: Value objects represent concepts without identity and are defined solely by their attributes. They can contain validation logic related to their attributes. For instance, an “EmailAddress” value object can ensure valid email formats. (email is value object because: An email address typically doesn’t have a unique identifier, it is identified by the email string itself. Two email addresses with the same value represent the same concept.)

Example:

class EmailAddress(val value: String) {
init {
if (!value.matches(Regex("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"))) {
throw BusinessValidationException("Invalid email address format.")
}
}
}
  • Domain Services: Business logic validation that can be placed in a domain service often involves more complex validation scenarios that go beyond the scope of a single entity or value object.
class OrderService {
fun validateOrderTotal(order: Order) {
val total = order.calculateTotal()
if (total <= 0) {
throw BusinessValidationException("Order total must be greater than zero.")
}
}
}

In summary, while determining where to place validation logic in DDD, you should consider the following factors:

  1. Rule Complexity: Consider how tricky or straightforward the rule is. Simple rules can often sit with the things they’re checking (entities or value objects), but complex ones may need domain service.
  2. Reusability: If you think you might need the same rule in various places in your software, it’s smart to put it in a central location for easy reuse — we call this place a ‘domain service.’

example: Imagine you’re developing an e-commerce platform, and you have a business rule that states “all new customers must confirm their email addresses before they can place an order.” This rule needs to be applied in various parts of your application, such as during the registration process, order placement, and customer account management.

Now, consider how this rule can be implemented:

Without a Domain Service:

If you don’t use a domain service, you’d need to write the email confirmation validation logic in multiple places in your application. For example, you’d have to include the validation in the registration process code and repeat it in the order placement code. This duplication makes your code harder to maintain, and if the rule ever changes, you’d need to update it in multiple places, which increases the risk of errors.

With a Domain Service:

However, if you create a domain service called CustomerValidationService, you can encapsulate the email confirmation validation logic in this central location. Whenever you need to check if a customer's email is confirmed, you can simply call the service. If the validation rule changes in the future, you only need to update it in one place (the domain service), ensuring consistent and reusable validation throughout your application.

class CustomerValidationService {
fun isEmailConfirmed(customer: Customer): Boolean {
return customer.emailConfirmed
}
}

3. Relationship to Your Domain: Think about whether the rule belongs to just one part of your software or if it applies to many. If it’s a ‘one-size-fits-all’ rule, it usually goes in the domain service.

--

--

Jyoti

Explorer, Observer, Curious and a head full of questions and thoughts.