Anemic Domain Model Anti-pattern article

Anemic Domain Model Anti-Pattern

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).

ADM is characterized by the following aspects:

  • Anemic domain objects are typically data structures without any behavior, often mirroring database tables.
  • Business logic is spread across various layers, leading to a lack of class expressiveness. The service layer becomes bloated as the model delegates work on its properties to the service layer.
  • Domain objects are often tightly coupled with service layers, violating encapsulation and separation of concerns.
  • OOD principles are violated. In extreme cases, services are simply a set of methods and then the system becomes procedural.

Drawbacks of the Anemic Domain Model:

  • Reduced data encapsulation. By separating behavior from domain objects, we are forced to open up the attributes of the model for modification by the service layer. In addition to the redundant propagation of knowledge about model attributes, this can lead to the existence of invalid instances in the runtime. In turn, this leads to a spreading of validations and checks in the components working with the object.
  • Decreased maintainability, increased complexity, and limited expressiveness. With business logic scattered across different layers, making changes or adding new features becomes more complex and error-prone; as well as understanding the model.

Why does the anemic model matter?

  • A rich domain model may introduce performance overhead due to defensive copying in the case of immutable models and repetitive validations. Some can be mitigated.
  • Possible compatibility issues with frameworks, particularly in mapping.
  • Developing a correct model is a challenging task. Sometimes (seldom) using an anemic model is a reasonable compromise.

How to avoid?

  • Follow the OO design principles.
  • Consider using private setters for your properties. This way you do not have to rely on external validations.
  • Use constructors with parameters. It’s essential to check passed parameters to initialize the object in the valid state.

Example

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.

Let’s start building something great together!

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.

tradeshift-integrator-team




    This site is protected by reCAPTCHA and the Google
    Privacy Policy and Terms of Service apply.

    SETRONICA


    Setronica is a software engineering company that provides a wide range of services, from software products to core business applications. We offer consulting, development, testing, infrastructure support, and cloud management services to enterprises. We apply the knowledge, skills, and Agile methodology of project management to integrate software development and business objectives effectively and efficiently.