A codebase's OrderApplicationService.placeOrder method has grown over two years to include inventory checks, tax calculation, fraud scoring, and shipping rule logic, all as inline conditionals, while the Order entity itself only has getters and setters. What is this anti-pattern called, why does it happen incrementally, and how would you refactor it?
answer
- anemic domain model = Fowler term
- getters/setters only, no behavior
- transaction script in OO clothing
- incremental 'just one more if' accretion
- strangler-style incremental fix
basics
~20 sThis is the anemic domain model anti-pattern — entities hold no behavior, logic piles into services. It happens because adding one more 'if' is easier than redesigning the entity. Fix: move each rule to the object owning its data.
solid answer
~40 sThis is the anemic domain model anti-pattern: entities become plain data holders while application services accumulate all business logic as procedural code — effectively a transaction script wearing OO clothing. It happens incrementally because each new rule is a small, locally-reasonable addition to an already-large method, and nothing automated flags the accumulating design smell. The refactor: for each rule, ask whether it concerns only one aggregate's own data (move it onto that aggregate as a method) or genuinely spans multiple aggregates or external policies (extract a domain service taking those objects as explicit parameters). Afterward the application service shrinks back to load, delegate, save — a short, readable sequence where each step's name reveals which rule is applied, even though the implementation now lives elsewhere.
go deeper
Can recognize that entities with only getters/setters and all logic in services 'feels off' even without naming the pattern.
Can name the anemic domain model pattern and give one concrete symptom (rule duplication).
Can walk through the incremental refactor, correctly sorting single-aggregate rules from true domain-service candidates, and can explain why the anti-pattern accretes without tests catching it.
Can propose review heuristics and codebase-health metrics to catch the trend early, and weigh strangler-style incremental fixes against the risk/cost of a big-bang rewrite.
## What the anti-pattern is This scenario describes the **anemic domain model** anti-pattern, a term Martin Fowler popularized to describe domain objects that are "little more than a bag of getters and setters" while all actual behavior lives in a separate service layer that operates on them from the outside. It's a specific and very common failure mode of the application-service/domain-service split: when the discipline of "business rules live in the domain layer" erodes, what's left behind structurally still looks like DDD (there are entities, there are services) but functionally behaves like the much older **transaction-script** style, where each use case is one long procedural method full of conditionals. ## Why it accretes rather than gets decided Understanding why it happens incrementally is important because it rarely happens as one deliberate decision — it accretes. When `placeOrder` already exists as a 40-line method handling loading, saving, and orchestration, and a new requirement arrives ("reject the order if any line item is out of stock"), the path of least resistance for the engineer under a delivery deadline is to add one more if-block right there, next to the code they're already looking at — rather than stopping to ask "should this be a method on `Order` instead?" and doing the larger refactor of moving state and behavior together. Each individual addition is small and locally defensible; nobody sits down and designs an anemic model on purpose. Compounding this, there's no automated signal that fires when this happens — unit tests still pass, the build still compiles, code review often approves "just one more validation check" without stepping back to see the accumulated shape of the method. Two years and a dozen such additions later, the method is unreadable, and untangling it requires understanding which of the now-dozen conditionals are truly orchestration-order concerns and which are business rules that need a new home. ## The production symptoms The concrete production symptoms of this anti-pattern are worth naming because they're what makes it costly rather than just aesthetically displeasing. 1. **First, rule duplication**: because `Order` itself exposes no `canBeCancelled()` or `calculateTotal()` method, every new use case that needs to check similar conditions (a bulk-cancel endpoint, a scheduled expiration job, an admin override tool) reimplements its own version of the check, and these versions drift apart over time — exactly the "works on mobile, fails on admin tool" bug class. 2. **Second, testability collapse**: testing the fraud-scoring rule requires spinning up or mocking the whole `placeOrder` use case with a database, a security context, and every other collaborator the method happens to need, rather than constructing an `Order` and calling one pure method — so edge-case coverage of the actual business rules gets expensive and, empirically, sparse. 3. **Third, onboarding cost**: a new engineer trying to understand "what are the rules for placing an order" has to read one giant procedural method instead of finding cohesive rule-methods on the `Order` type where a domain-savvy reader would expect to look. ## The refactor The refactor is mechanical but requires judgment about ownership, per the earlier funds-transfer reasoning: for each conditional in the bloated method, ask whether the rule concerns only the data already inside one aggregate (in which case it becomes a method on that aggregate — `order.checkInventory(stockLevels)`, `order.calculateTax(taxRules)` if the tax rate is a simple lookup passed in) or whether it inherently needs multiple aggregates or an external policy object to evaluate (fraud scoring against the customer's order history and an external risk-scoring service, or shipping-rate calculation involving a carrier-rate table) — in which case it becomes a domain service that takes the relevant domain objects and policy objects as explicit parameters and returns a result or throws a domain exception. After the refactor, `placeOrder` shrinks back to: - load the cart and customer - call `order.checkInventory(...)` - call `fraudScoringService.assess(order, customer)` - call `order.calculateTax(...)` - save - publish event - map to DTO That is a short, readable sequence where each step's name tells you what rule is being applied, even though the rule's implementation now lives elsewhere. ## The trade-off worth being honest about One trade-off worth being honest about: this refactor is not free, and doing it as a "let's rewrite everything" big-bang effort on a two-year-old method is itself risky (regressions, merge conflicts with in-flight feature work). The pragmatic approach most teams use is the **strangler style** — the next time you touch one of the conditionals for a real feature change, extract just that rule to its proper home and add a unit test for it there, rather than trying to fix the whole method in one pass. Over several iterations the method shrinks back toward pure orchestration without ever requiring a dedicated refactor sprint that's hard to justify to stakeholders.
- How would you catch this anti-pattern forming during code review, before it grows into a 40-line method?Watch for any new conditional added to an application-service method that references only fields of one already-loaded aggregate — that's a strong candidate to be pushed onto the aggregate instead of staying inline. A simple review heuristic: if you can rewrite the if as a call to a new method on the aggregate, do it before merging rather than after.
- Does an anemic domain model always indicate the application-vs-domain-service line was misdrawn, or can it happen for other reasons?It usually does trace back to that line being blurred, but it can also result from teams intentionally choosing a simpler transaction-script style for genuinely simple CRUD-heavy domains where DDD's tactical patterns add more ceremony than value — the anti-pattern label really only applies when the domain has real behavioral complexity that's being flattened into procedural code rather than modeled.
- What's a fast way to measure whether a codebase is trending anemic over time?Track the ratio of lines of code or cyclomatic complexity in application-service classes versus domain entity/service classes over successive releases; a steadily rising ratio in the application-service side, especially with growing branch counts per method, is a leading indicator worth flagging before it becomes a costly rewrite.
It's like a company where the org chart still shows department heads (entities) but every actual decision gets made by one overworked coordinator (the application service) who's memorized everyone's job because nobody trusted the department heads to own their own decisions — the coordinator becomes a single point of complexity and failure.
saying these in an interview costs you the question
- Calls this 'just normal service-layer code' with no concern
- Proposes fixing it by adding yet another application-service method rather than moving behavior to the domain objects
- Suggests a full rewrite is the only viable fix, ignoring incremental strangler-style refactoring
- Can't distinguish which conditionals are single-aggregate rules vs true domain-service candidates