Anton Bogomazov | June 2nd, 2023
This anti-pattern is so widespread that the average developer doesn’t even consider that things could be different. In extreme cases, they may even view a non-anemic model as something strange and incorrect. Therefore, I believe it is important to draw attention to the shortcomings of what people have become accustomed to working with and how things could be different.
The Anemic Domain Model (AMD) refers to a very common anti-pattern where the domain model objects lack behavior and encapsulate only data. The business rules and operations are typically implemented in separate service layers.
Firstly, I want to note that ADM is not limited to domain-driven designed systems; it applies to any object-oriented system. As Martin Fowler stated, “The fundamental horror of this anti-pattern is that it’s so contrary to the basic idea of object-oriented design, which is to combine data and process together.” Separating behavior from data leads to a lack of cohesion between them and reduced data encapsulation.
Also, it’s essential to highlight that there are cases where a simple data structure is sufficient, e.g. data transfer object (DTO).
In the example below, there is a component that calculates tax when a product is sold using a tax provider that encapsulates local tax policies. All behaviour related to the Product is localized in the ProductService. This design in no way restricts the existence of invalid instances, nor the propagation of knowledge about object fields. Moreover, we are forced to repeat the idiom if (!isValid(product)) throw invalidProductError()
from method to method when validating the passed product.
class Product(
var id: ProductId,
var name: String,
var price: Long,
var shipTo: Location,
)
class ProductService(
@Inject private val taxProvider: TaxProvider,
@Inject private val repository: ProductRepository,
) {
fun calculateTax(product: Product): Tax {
if (!isValid(product)) throw invalidProductError()
return taxProvider.getTax(product.price, product.shipTo)
}
fun save(product: Product) {
if (!isValid(product)) throw invalidProductError()
repository.save(product.toEntity())
}
private fun isValid(product: Product): Boolean {
return product.name.isNotBlank() && product.price > 0
}
}
How to do it better:
class Product private constructor(
val id: ProductId,
val name: String,
val price: Long,
private val shipTo: Location,
) {
companion object {
fun create(
idProvider: IdProvider,
name: String,
price: Long,
shipTo: Location,
): Product {
if (name.isBlank() && price <= 0) throw invalidProductError()
return Product(
id = idProvider.getNext(),
name = name,
price = price,
shipTo = shipTo,
)
}
}
fun calculateTax(taxProvider: TaxProvider) {
return taxProvider.getTax(this.price, this.shipTo)
}
}
The existence of an invalid instance is impossible, each new one can be created exclusively with a static factory method, which already contains validation. With this design, it became obvious that the class was mixing several responsibilities: tax calculation, and db operations, so the save method was moved to ProductPersister (left out). We also limited the mutability of the fields and excessive visibility of the shipTo attribute.
In conclusion, the Anemic Domain Model (ADM) represents a common anti-pattern where domain model objects lack behavior, resulting in reduced cohesion and data encapsulation. This issue extends beyond domain-driven design and violates fundamental principles of object-oriented design. The drawbacks include decreased maintainability, complexity, and limited expressiveness. Anemic models can lead to compatibility issues, reduced performance, and challenges in developing a correct model. To avoid this anti-pattern, adhere to OO design principles, use private setters, and employ constructors with parameters to ensure valid object initialization.
Contact us today to discuss your project and see how we can help bring your vision to life. To learn about our team and expertise, visit our ‘About Us‘ webpage.