Skip to main content

Anemic Domain Model

Domain objects that are just data containers, with business logic scattered elsewhere.

TL;DR

An Anemic Domain Model treats domain objects as mere data containers with getters/setters, while all business logic resides in service classes. This violates object-oriented design principles, scatters domain knowledge across your codebase, makes testing harder, and creates brittle systems where business rules are implicit and hard to enforce.

Learning Objectives

You will be able to:

  • Identify characteristics of anemic domain models in existing codebases
  • Understand why anemic models violate core OOP principles and Domain-Driven Design
  • Refactor an anemic model into a rich domain model with encapsulated behavior
  • Design domain objects that enforce business invariants
  • Test domain logic in isolation without service layer complexity

Motivating Scenario

Imagine you're building an e-commerce platform. You start by creating an Order class that just holds data: items, customer, total, status. All the logic for calculating totals, applying discounts, checking inventory, and validating orders lives in OrderService, DiscountService, InventoryService, and PaymentService.

Six months later, you need to implement a new discount rule: "Orders over $500 get 10% off." You hunt through three service classes before finding the discount calculation. You update it, but miss a edge case in another service that duplicates the logic. Three weeks after shipping, a customer reports incorrect pricing. Now imagine maintaining this across 50+ domain objects, each with scattered business rules in a dozen different service classes. The cost of change becomes unbearable.

Core Explanation

An anemic domain model treats domain objects as data transfer objects (DTOs) with only accessors and mutators. The actual business logic—the rules that govern how your domain works—lives in separate service classes. This creates several problems:

1. Violation of Object-Oriented Encapsulation

Objects should encapsulate both data AND the behavior that operates on that data. By separating them, you lose the primary benefit of object orientation: the ability to reason about data and its valid transformations together.

2. Scattered Business Rules

When business logic lives in services, the same domain rule often gets implemented in multiple places. An Order calculation might be in OrderService.calculateTotal(), but there's also logic in InvoiceService.generateInvoice() that does similar calculations. Change one, forget the other, and you have a bug.

3. Difficult Testing

Testing business logic requires instantiating the entire service layer with all its dependencies. You can't easily test order calculations without mocking database layers, payment gateways, and notification services. This makes tests slow, brittle, and expensive to maintain.

4. Implicit Business Rules

In an anemic model, the rules that should always be true about an order (like "an order can't be shipped without items") aren't enforced in the object itself. They're buried in service code as comments or scattered if-statements.

5. Harder Domain Understanding

New developers joining the team can't read the Order class to understand what an order is. They have to search through multiple services to understand the domain. This increases onboarding time and introduces subtle bugs.

A rich domain model, by contrast, encapsulates both data and behavior. Business rules live where the data lives, making them explicit, enforceable, and testable in isolation.

Pattern Visualization

Anemic vs Rich Domain Model Architecture

Code Examples

order.py
class Order:
def __init__(self):
self.items = []
self.customer = None
self.total = 0
self.status = "pending"

def get_items(self):
return self.items

def set_items(self, items):
self.items = items

def get_total(self):
return self.total

def set_total(self, total):
self.total = total

# No business logic here!

class OrderService:
def calculate_total(self, order):
total = 0
for item in order.get_items():
total += item.price * item.quantity
return total

def apply_discount(self, order, discount_percent):
# Discount logic scattered here
if discount_percent > 50:
raise ValueError("Discount too high")
current_total = self.calculate_total(order)
discount_amount = current_total * (discount_percent / 100)
order.set_total(current_total - discount_amount)

def validate_order(self, order):
if len(order.get_items()) == 0:
raise ValueError("Order must have items")
if order.customer is None:
raise ValueError("Order must have customer")

Patterns and Pitfalls

Anti-patterns Found in Anemic Models

1. The Service God Class Problem When all business logic moves to services, those services become "god classes" doing everything. An OrderService ends up with 50+ methods handling calculations, validation, notifications, and payments.

2. Duplicate Validation The same validation rule appears in multiple services. "Check if order has items" might exist in OrderService, InvoiceService, ShippingService, and PaymentService.

3. Implicit Dependencies You can't look at an Order object and understand what makes a valid order. The rules are hidden in service code, making the domain knowledge implicit and fragile.

4. Test Bloat Tests must set up entire service hierarchies to test a simple business rule. A test for "applying a 10% discount" requires mocking databases, payment systems, and notification services.

Why Developers Default to Anemic Models

  • Databases think in tables: ORM patterns encourage thinking in terms of entities with getters/setters
  • Web frameworks encourage separation: MVC, REST APIs naturally separate data from logic
  • Familiar from simple CRUD: Many developers learn anemic patterns in tutorial applications
  • Perceived simplicity: It seems simpler to write getters/setters than to design rich objects

When This Happens / How to Detect

Red Flags in Your Codebase:

  1. All classes have only getters and setters
  2. Service classes are 500+ lines
  3. You can't find where a business rule is implemented
  4. The same validation appears in 3+ places
  5. Tests require instantiating entire service graph
  6. Adding a business rule requires changes in 5+ files
  7. Domain objects have no meaningful methods
  8. You can't understand what an object represents by reading its class

How to Fix / Refactor

Step 1: Identify Scattered Business Logic

Grep through your codebase for methods operating on an Order object:

grep -r "calculateTotal" src/
grep -r "applyDiscount" src/
grep -r "validateOrder" src/

Step 2: Extract to Domain Object

Move these methods onto the domain class itself. Start small with one object. Don't try to refactor everything at once.

Step 3: Enforce Invariants

Add validation in constructors and mutators. Make it impossible to create an invalid Order:

# Bad: Validation happens elsewhere
order = Order()
order.total = -100 # No prevention

# Good: Validation in constructor
order = Order(items, customer) # Fails immediately if invalid

Step 4: Hide Implementation Details

Use private/protected fields. Callers shouldn't directly access order.items, they should call order.calculateTotal().

Step 5: Provide Meaningful Methods

Replace getItems() with methods that express intent:

# Instead of:
total = sum(item.price * item.quantity for item in order.get_items())

# Do this:
total = order.calculate_total()

Step 6: Test in Isolation

Now you can test business logic without any service layer:

def test_discount_applied_correctly():
order = Order(customer)
order.add_item(OrderItem('PROD-1', 100, 1))
order.apply_discount(10)
assert order.calculate_total() == 90

Operational Considerations

Migration from Anemic to Rich Models:

  • Gradual refactoring: Don't rewrite everything at once. Move one object at a time
  • Backward compatibility: Keep old service methods as deprecated wrappers while transitioning
  • Testing safety net: Write tests for domain behavior before refactoring, to catch regressions
  • Documentation: Document domain invariants explicitly in the class

Performance Implications:

  • Rich models often perform better (less service orchestration)
  • No N+1 problems from service layers calling each other
  • Easier to optimize because logic is colocated

Team Adoption:

  • Domain-Driven Design training helps team understand the benefits
  • Code review focus on moving logic toward domain objects
  • New features should use rich models from day one

Design Review Checklist

  • Does each domain object have business logic, not just getters/setters?
  • Are domain invariants enforced in the object itself, not in services?
  • Can you test domain logic without service dependencies?
  • Are business rules co-located with the data they operate on?
  • Do domain objects have meaningful method names expressing intent?
  • Is validation happening in the domain object constructor/mutators?
  • Are there no duplicate implementations of the same business rule?
  • Can a new developer understand the domain by reading the objects?
  • Are service classes focused on orchestration, not business logic?
  • Is it impossible to create invalid domain objects?
  • Do domain objects hide their internal state with private/protected fields?
  • Are state transitions (e.g., pending→confirmed) enforced on the object?

Showcase

Signals of Anemic Domain Model

  • Order class with only getters/setters
  • calculateTotal() method in OrderService
  • validateOrder() duplicated in 3 places
  • Tests require mocking 10+ dependencies
  • Business rules in comments scattered across services
  • Impossible to understand what's a valid Order without reading 5 files
  • Order.calculateTotal() method on Order class
  • Order.validate() enforced in constructor
  • Order.applyDiscount() with validation built-in
  • Single source of truth for each business rule
  • Unit tests just instantiate Order without service layer
  • New developers can read Order class and understand the domain

Self-Check

  1. Can you identify where a specific business rule is implemented in 10 seconds? If not, it's scattered across services.

  2. Do your domain object tests require mocking external services? If yes, the logic should be on the domain object, not the service.

  3. Could you implement the same validation in 2-3 different places? If yes, move it to the domain object where it's guaranteed to be enforced.

Next Steps

  • Read: Single Responsibility Principle ↗️ to understand why services should focus on orchestration
  • Study: GRASP: Information Expert ↗️ for guidance on where domain logic should live
  • Explore: Domain-Driven Design patterns for structuring complex domains
  • Practice: Refactor one service's business logic into a domain object this week
  • Review: Examine your codebase for scattered validation logic to refactor

One Takeaway

ℹ️

Move business logic from service classes into domain objects. Make invalid states impossible. Encapsulate both data and behavior together—that's the whole point of objects.

References

  1. Martin Fowler - Anemic Domain Model ↗️
  2. Eric Evans - Domain-Driven Design ↗️
  3. Refactoring Guru - Extract Method ↗️
  4. SOLID Principles ↗️