DEV Community

Xuan
Xuan

Posted on

The OOP Lie: Your Domain Model Is Anemic & You Don't Even Know It!

Here is your blog post:

The OOP Lie: Are You Building Anemic Models?

Ever feel like your carefully crafted object-oriented programs aren't quite living up to the hype? You’ve got classes, interfaces, inheritance, maybe even some fancy design patterns. Yet, your "domain model" — the heart of your business logic — feels… empty. You might have stumbled into a common trap: the Anemic Domain Model. And the scary part? Many of us don't even realize it.

Let's break it down.

What's an "Anemic Domain Model," Anyway?

Imagine your application is a human body. Your "domain model" should be the brain, the muscles, the organs – the parts that actually do things. It should contain both the data (what something is) and the behavior (what something does).

An Anemic Domain Model is like a body with a brain that can only store information but can't think, and muscles that can't move themselves. In code terms, it means your core business objects (like Order, Product, Customer) are just bags of data. They have properties (like OrderId, ProductName, CustomerAddress) but very few, if any, methods that perform actual business operations.

Instead, all the real work happens outside these objects, usually in separate "service" classes. So, you might have an Order object with getOrderId() and setTotal(), but the logic for calculateDiscount() or processPayment() lives in an OrderService.

Why Is This a Problem? (The "Lie" Part)

This isn't just about sounding fancy. It has real consequences:

  1. Lost Business Logic: Your business rules become scattered across many service classes. If you want to understand how an order is processed, you have to jump through multiple files, piecing together the puzzle. This makes your code harder to understand, maintain, and change.
  2. Less Reliable Code: When logic is spread out, it's easier to make mistakes. Two different service methods might implement slightly different versions of the same rule, leading to inconsistencies.
  3. Harder to Test: Testing individual service methods in isolation might seem easy, but testing the entire flow of a business operation becomes complex because you're coordinating multiple service calls.
  4. Not Truly "Object-Oriented": The core idea of OOP is to encapsulate data and behavior together. An anemic model violates this by separating them. It's more like procedural programming dressed up in class syntax.

How Did We Get Here? Common Traps:

  • Database-First Thinking: We often start by designing our database tables, and then our classes simply mirror those tables. This leads to data-focused objects.
  • "Services Do Everything" Mentality: It feels natural to put logic in "services." After all, they "serve" a purpose. But this often becomes a dumping ground.
  • Fear of "Big Objects": Sometimes we worry about making our objects "too big" or "too complex" by adding methods. But a rich domain object is often simpler to understand in context.

Breaking Free: Building Rich Domain Models (The Solution!)

The good news? It's fixable! The solution is to empower your domain objects to do things.

Here’s how to start:

  1. Identify Behavior, Not Just Data: When you think about a Product, don't just list its properties. Ask: What can a Product do? Can it adjustPrice()? Can it goOutOfStock()? Can it addReview()?
  2. Move Logic INTO the Object: If a piece of logic primarily uses the data within an object and affects the state of that object, it probably belongs as a method on that object.
    • Instead of OrderService.calculateTotal(order), make it order.calculateTotal().
    • Instead of CustomerService.updateAddress(customer, newAddress), make it customer.updateAddress(newAddress).
  3. Use "Tell, Don't Ask": This is a powerful principle. Instead of asking an object for its data and then making a decision outside of it, tell the object to do something.
    • Bad (Ask): if (order.getStatus() == OrderStatus.Pending) { OrderService.processPayment(order); }
    • Good (Tell): order.processPayment(); (The order object itself knows if it's in a state where it can process payments.)
  4. Embrace Value Objects: For things that represent a concept but don't have a unique identity (like an Address, MoneyAmount, or DateRange), make them immutable objects with their own logic. For instance, an Address could have a getFullAddressString() method.
  5. Services for Orchestration (Not Logic): Services still have a role! They should coordinate interactions between different domain objects, handle database transactions, or integrate with external systems. They orchestrate the flow, but they don't contain the core business rules themselves.
    • Example: An OrderService might createOrder(), which involves customer.placeOrder(cart), then inventory.deductStock(orderItems), and finally paymentGateway.processTransaction(order). The core logic of placing an order or deducting stock resides within the Customer and Inventory objects.

An Example in Action:

Let's imagine an Account object in a banking app:

Anemic (Bad):

class Account {
    private double balance;
    // Getters and Setters
}

class AccountService {
    public void deposit(Account account, double amount) {
        if (amount > 0) {
            account.setBalance(account.getBalance() + amount);
        } else {
            throw new IllegalArgumentException("Deposit amount must be positive.");
        }
        // Save to database
    }

    public void withdraw(Account account, double amount) {
        if (amount > 0 && account.getBalance() >= amount) {
            account.setBalance(account.getBalance() - amount);
        } else {
            throw new IllegalArgumentException("Invalid withdrawal.");
        }
        // Save to database
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice how all the rules (amount > 0, balance >= amount) are outside the Account itself.

Rich (Good):

class Account {
    private double balance;

    public Account(double initialBalance) {
        if (initialBalance < 0) {
            throw new IllegalArgumentException("Initial balance cannot be negative.");
        }
        this.balance = initialBalance;
    }

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit amount must be positive.");
        }
        this.balance += amount;
    }

    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Withdrawal amount must be positive.");
        }
        if (this.balance < amount) {
            throw new IllegalStateException("Insufficient funds.");
        }
        this.balance -= amount;
    }

    public double getBalance() {
        return balance;
    }
}

// AccountService now just coordinates or persists
class AccountService {
    public void performDeposit(Account account, double amount) {
        account.deposit(amount); // Business logic handled by Account
        // Save account to database
    }

    public void performWithdrawal(Account account, double amount) {
        account.withdraw(amount); // Business logic handled by Account
        // Save account to database
    }
}
Enter fullscreen mode Exit fullscreen mode

In the "Rich" example, the Account object is now responsible for ensuring its own integrity and enforcing its own rules. This makes the code clearer, safer, and truly object-oriented.

Wrapping Up

The "OOP Lie" isn't about OOP being bad, but about a common misinterpretation that leads to less effective systems. By empowering your domain objects with their own behavior, you create a more robust, understandable, and maintainable application. Start looking for those "bags of data" in your codebase, and ask yourself: "What can this object do?" You might be surprised by how much cleaner and more powerful your code becomes.

Top comments (0)