open atlas
↑ Back to track
Code patterns & craft CP · 13 · 01

God object & anemic domain model

Two structural anti-patterns pull in opposite directions: the god object knows and does everything, while the anemic model has data classes with no behavior at all. The fix for one must not become the other — move behavior to its data, spread across cohesive objects.

CP Senior ◷ 20 min
Level
FoundationsJuniorMiddleSenior

Two reviewers look at the same backend. One says “this OrderService is a god object — it’s 2,000 lines and touches everything.” The other looks at the Order class it operates on and says “but Order is anemic — it’s just getters and setters, there’s no logic in it at all.” Both are right, and they’re describing the same problem from two ends.

These two anti-patterns are a matched pair. The god object holds all the behaviour; the anemic model holds all the data; and the line between them is exactly where object-orientation broke. Fixing one naively produces the other — drain the service to “fix” anemia and you can fatten an entity straight into a new god object. The senior skill is holding both failure modes in view at once.

Goal

After this lesson you can recognise a god object as a coupling singularity (one class everything depends on), recognise an anemic domain model as data-bags-plus-procedural-service (“OO in name only”), explain why the cure for anemia is moving behaviour into the data it operates on, and — critically — name the failure mode where that cure overshoots and turns a rich entity into the next god object.

1

A god object is a bloater at architecture scale — a single class that knows and does everything. A long method is a bloater inside one function; a god object is the same decay one level up: one class accumulates responsibilities until it becomes the centre of the system. Everything calls into it, so everything depends on it, so nothing can change without risking it.

class OrderService {
  createOrder() {/* ... */}
  validateInventory() {/* ... */}
  chargeCard() {/* ... */}
  calculateTax() {/* ... */}
  applyDiscount() {/* ... */}
  sendConfirmationEmail() {/* ... */}
  generateInvoicePdf() {/* ... */}
  scheduleShipment() {/* ... */}
  refund() {/* ... */}
  // ...60 more methods, 2,000 lines
}

The tell isn’t line count alone — it’s that the class has many unrelated reasons to change (tax law, payment provider, email template, PDF layout). It is a coupling singularity: a change to any one concern risks all the others, and every test must construct this whole universe to test a corner of it.

2

An anemic domain model is the opposite shape — data classes with no behaviour, and all logic in a separate procedural layer. Fowler named this: objects that are pure state — getters and setters — with the rules that govern that state living outside them, in “services”. It looks object-oriented (you have an Order class) but it is OO in name only: it’s procedural code with the data and the procedures pulled apart.

class Order {
  status: string;
  items: Item[];
  discountCents = 0;
  // ...only fields + get/set. No methods that mean anything.
}

// all real behaviour lives here, reaching INTO the data
class OrderService {
  applyDiscount(order: Order, pct: number) {
    if (order.status !== "open") throw new Error("closed");
    order.discountCents = Math.round(total(order.items) * pct);
  }
}

The cost: Order’s invariants (you can’t discount a closed order; a discount can’t exceed the total) are not protected by Order. Any code holding the object can set discountCents to nonsense. The data and the rules that keep it valid live in different files, so every reader has to assemble the real object in their head.

3

The cure is the same one principle: behaviour belongs with the data it operates on. Move the rule from the service into the entity, so the object enforces its own invariants. This is “tell, don’t ask” — instead of asking an order for its fields and deciding outside, you tell the order to do the thing and let it guard itself.

class Order {
  private status: "open" | "closed" = "open";
  private items: Item[];
  private discountCents = 0;

  applyDiscount(pct: number): void {
    if (this.status !== "open") throw new Error("cannot discount a closed order");
    if (pct < 0 || pct > 1) throw new Error("invalid discount");
    this.discountCents = Math.round(this.subtotal() * pct);
  }

  private subtotal(): number {
    return this.items.reduce((s, i) => s + i.priceCents * i.qty, 0);
  }
}

Now the invariant is unbreakable from outside: discountCents is private, and the only path to change it goes through a method that checks the rules. The rule lives next to the data it constrains, which is the whole point of an object. The procedural service shrinks; the model gets rich.

4

The trap: “fixing” anemia by cramming all logic into one entity recreates the god object. This is the failure mode that catches people who learned “rich model = good”. If every rule that mentions an order goes onto Order, then tax tables, payment gateways, PDF rendering, and shipment scheduling all migrate onto one class — and you have rebuilt OrderService, just spelled Order. You swapped an anemic data-bag plus a god service for a single god entity.

// OVERSHOOT — anemia "fixed" into a god entity
class Order {
  applyDiscount(pct: number) {/* belongs here ✓ */}
  chargeCard(gateway: PaymentGateway) {/* a payment concern ✗ */}
  renderInvoicePdf() {/* a presentation concern ✗ */}
  calculateTax(rates: TaxTable) {/* a tax-policy concern ✗ */}
}

The discriminator is cohesion, not “more methods on the entity”. Behaviour belongs with its data when it operates on that object’s own state and protects that object’s own invariants. applyDiscount reads and constrains the order’s items and status — it belongs. chargeCard orchestrates an external gateway; renderInvoicePdf is presentation; calculateTax is policy that varies independently. Those are different responsibilities and belong on different cohesive objects (a Money/Discount value object, a TaxPolicy, a PaymentGateway), not piled onto Order. The goal was never “fat entities” — it was behaviour distributed across many cohesive objects, each owning its own data.

Worked example

One responsibility, three placements. Start from the anemic shape — a BankAccount data-bag and a service that reaches into it:

class BankAccount {
  balanceCents = 0;
  frozen = false;
}

class AccountService {
  withdraw(acc: BankAccount, cents: number) {
    if (acc.frozen) throw new Error("frozen");
    if (cents > acc.balanceCents) throw new Error("overdraft");
    acc.balanceCents -= cents;
  }
}

The invariants (“no withdrawal from a frozen account”, “no overdraft”) are not guarded by BankAccount — any code can write acc.balanceCents = -999. Now move the rule into the data, where it can defend itself:

class BankAccount {
  private balanceCents = 0;
  private frozen = false;

  withdraw(cents: number): void {
    if (this.frozen) throw new Error("account is frozen");
    if (cents > this.balanceCents) throw new Error("insufficient funds");
    this.balanceCents -= cents;
  }
}

That is the right cure: behaviour that operates on the account’s own state now lives on the account, and the invariant is enforced at the only door. But watch the overshoot — don’t keep going and bolt on everything that merely mentions an account:

class BankAccount {
  withdraw(cents: number) {/* its own state ✓ */}
  sendMonthlyStatementEmail() {/* notification concern ✗ */}
  reportToCreditBureau() {/* integration concern ✗ */}
  calculateInterestForTaxYear(rates: TaxTable) {/* policy concern ✗ */}
}

withdraw belongs because it touches the account’s own balance and protects its own invariants. The other three pull in email infrastructure, an external bureau API, and tax policy — independent reasons to change — so they belong on a StatementMailer, a CreditReporter, and an InterestPolicy. Keep BankAccount rich about its own money rules and ignorant of everyone else’s job. That’s the difference between a rich model and a god entity.

Why this works

Why is “behaviour with its data” the discriminator, rather than a method count? Because the real cost both anti-patterns impose is the same: a reader can’t understand the object in one place. In the anemic model the rules are scattered into services, so you read three files to know what an Order can do. In the god object the responsibilities are scattered onto one class, so you read 2,000 lines to find the three that matter. Cohesion — each unit doing one related job with its own data — is what keeps both the reading cost and the change blast-radius small. “Rich model” is not the goal for its own sake; it is the shape that happens to maximise cohesion for state-and-invariant logic.

Common mistake

The most common mistake after learning this is treating “anemic” as an absolute slur — ripping all behaviour into every entity, including DTOs at the system boundary and read-model projections that genuinely should be dumb data. Not every data class is an anemic domain model. A transport DTO, an API response shape, or a CQRS read model is allowed to be pure data — it has no invariants to protect. The anti-pattern is specifically a domain object with real invariants whose rules were exiled to a service. Apply the cure to domain entities that own business rules; leave boundary data structures plain. Misfiring this turns a clean DTO layer into a tangle of misplaced logic.

Check yourself
Quiz

A team has an anemic 'Order' (getters/setters only) and a 1,500-line 'OrderService'. A junior 'fixes the anemia' by moving every method from OrderService onto Order — including chargeCard(), renderInvoicePdf(), and calculateTax(). What happened?

Recap

The god object and the anemic domain model are a matched pair on one axis. A god object is a bloater at architecture scale — one class that knows and does everything, a coupling singularity that everything depends on. An anemic domain model is its mirror: data classes with getters and setters but no behaviour, all rules exiled to a procedural service, so it is OO in name only. The cure for anemia is one principle — move behaviour into the data it operates on, so each object enforces its own invariants (“tell, don’t ask”). But the cure has a failure mode: cram every rule that mentions an entity onto it and you rebuild the god object as a fat entity. The real discriminator is cohesion — behaviour belongs with data when it touches that object’s own state and protects its own invariants, and unrelated responsibilities belong on other cohesive objects. The target is never “fat entities”; it is behaviour distributed across many objects that each own their data.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.