A codebase has a class called OrderService with roughly 40 methods covering discount calculation, order validation, tax computation, and shipment scheduling, injected wherever an Order is touched. Is this a Domain Service in the DDD sense? What's wrong with it, and how would you fix it?
answer
- 40-method service = topic not process
- anemic entity is the real symptom
- SRP applies to domain modeling too
- triage: entity vs value object vs new service
- incremental extraction, not big-bang rewrite
basics
~20 sNo - a real Domain Service covers one business process, not everything vaguely Order-related. This is a dumping ground that hollowed out Order. Fix it: move Order's own logic back onto Order, split the rest into small, named services.
solid answer
~40 sThis is not a Domain Service in the DDD sense - it's an anemic-model symptom wearing a domain-sounding name. A genuine domain service encapsulates one cohesive piece of logic that doesn't belong to any entity; a 40-method class covering discounting, validation, tax, and shipping is really several unrelated responsibilities glued together because they all touch Order. The likely cause: Order was reduced to a data holder as its behavior was evacuated into this one service, the path of least resistance. The fix has two parts: identify methods that only need Order's own data and move those back onto the entity (or into value objects like Discount); for methods that genuinely span aggregates or represent standalone processes, split them into small, specifically-named services (TaxCalculator, ShipmentScheduler), done incrementally rather than as a risky rewrite.
go deeper
Should recognize that a 40-method class touching many unrelated concerns is a red flag, even without proposing a detailed fix.
Should be able to sort methods into 'belongs on the entity' versus 'genuinely cross-cutting' and explain why that distinction matters.
Should propose a concrete, low-risk triage and extraction plan, including how to avoid a risky big-bang rewrite.
Should diagnose the social/process root cause (why the class grew this way) and propose team practices - naming conventions, review checkpoints - that prevent recurrence across the codebase, not just fix this one class.
## Named like one versus behaving like one The first move in answering this is separating the question 'is it named like a domain service?' from 'does it behave like one?' A class named `OrderService` with 40 methods spanning discounting, validation, tax, and shipping is almost certainly **not a coherent domain service** in the DDD sense, because a genuine domain service is scoped to one specific business process expressible in a single sentence of the ubiquitous language - 'calculate the applicable discount' or 'schedule a shipment' - not to a vague topic area like 'stuff related to orders.' What this class actually is, in most real codebases that arrive at this shape, is the **residue of an anemic domain model**: over time, developers under time pressure default to adding 'just one more Order-related method' to the existing service because that's less friction than deciding whether the logic belongs on the `Order` entity, on a value object, or in a new, focused service - and the service grows without anyone ever stepping back to ask what its single responsibility actually is. ## How the drift happens The mechanism behind this drift is almost always **social and incremental** rather than a single bad decision: the first few methods on `OrderService` might have been legitimate domain-service candidates (logic spanning `Order` and another aggregate), but each subsequent addition rode on the credibility of 'well, there's already an OrderService, I'll just add my method there,' regardless of whether the new method's logic actually needed anything beyond the `Order` entity itself. This is exactly how a class violates the **single-responsibility principle at the domain-modeling level**, not just at a generic OOP-hygiene level - it isn't just 'too big,' it represents an incoherent mixture of business concepts that happen to share a noun. ## What the anti-pattern costs The cost of this anti-pattern shows up in several concrete ways. - **Testability suffers first**: a class with 40 methods typically has a correspondingly large and tangled set of dependencies (repositories, external tax APIs, shipping providers, discount rule engines), so testing one unrelated method drags in setup for collaborators it doesn't even use, and mocking becomes a chore rather than a safety net. - **Readability for domain experts suffers next**: if a business stakeholder asks 'where does the system decide whether a customer qualifies for free shipping,' the answer 'somewhere inside this 40-method class, good luck finding it' is a direct failure of the DDD goal that code should mirror the business's own mental model. - **And change risk compounds over time**: because the class is depended on everywhere ('injected wherever an Order is touched'), even a small change made for the discounting logic risks breaking the unrelated tax logic sitting in the same file, purely because they're compiled and deployed as one unit with shared internal state or helper methods that were never meant to be shared. ## The entity is the other half of the failure The underlying entity, `Order`, is the other half of the failure - its own class has likely degenerated into little more than fields and getters, because every piece of behavior that should live there ('can this order still be cancelled,' 'apply this line-item change') was instead written as a method on `OrderService` that takes an `Order` parameter and mutates it externally. That's the **anemic-domain-model pattern by definition**: data and behavior have been pulled apart, so nothing enforces that Order's invariants are protected wherever its state changes, since any of those 40 external methods could, in principle, mutate it incorrectly without `Order` itself having a say. ## The fix is a triage, not a rewrite The fix is a triage, not a rewrite. Go through the 40 methods and sort them into three buckets. 1. **The first bucket is methods whose logic depends entirely on Order's own fields** (e.g., a method computing whether the order is still within its cancellation window) - these move back onto the `Order` entity itself, restoring behavior to where it belongs and immediately making Order's own invariants enforceable in one place again. 2. **The second bucket is methods that are genuinely standalone domain processes** involving other aggregates or external domain concepts (tax computation that depends on jurisdiction rules, shipment scheduling that depends on carrier availability) - these become their own small, specifically-named domain services (`TaxCalculator`, `ShipmentScheduler`), each with a narrow, testable responsibility and its own minimal set of collaborators. 3. **The third bucket is methods that turn out, on inspection, to be pure calculations** over a handful of values that could be expressed as a value object's own method (a `Discount` value object with an `apply(Money)` method, for instance) - these move into value objects rather than services at all. This triage is best done **incrementally, method by method, behind the existing call sites**, specifically to avoid the big-bang-rewrite risk of trying to redesign the whole thing in one pass while the system is still live - each extraction can be verified independently, and the `OrderService` class shrinks gradually until, ideally, it's deleted entirely once every method has found its real home.
- How would you convince a team to invest time splitting up an OrderService like this when it's not causing an outright production incident?Point to concrete, measurable costs they already feel - slow or brittle tests, frequent unrelated merge conflicts in the same file, or onboarding friction where new engineers can't find where a given business rule lives - and propose the incremental triage as low-risk, since each extraction is independently verifiable rather than a risky rewrite.
- What's the difference between a legitimate multi-method domain service and this anti-pattern?A legitimate domain service's methods all serve one cohesive business process and typically share the same small set of collaborators; this anti-pattern's methods serve unrelated processes (discounting, tax, shipping) that only share a noun (Order), which is a much weaker form of cohesion than a shared process.
- Should the fixed-up services still be called 'OrderXxxService', or does that risk repeating the same drift?Naming them after the specific process (TaxCalculator, ShipmentScheduler) rather than tying every name back to Order is safer, since it removes the implicit invitation to add 'anything Order-related' to a service just because Order is in its name; the service's dependency on Order, if any, is expressed through its method parameters, not its class name.
A 40-method OrderService is like a junk drawer in a kitchen - it started with a couple of genuinely miscellaneous items, but because it already existed and had room, everything without an obvious home got tossed in, until nobody can find anything and half the drawer's contents don't belong in the kitchen at all.
saying these in an interview costs you the question
- Defends the 40-method class as fine because 'it's all about orders'
- Proposes renaming the class without changing its actual responsibilities
- Wants to do a full big-bang rewrite instead of incremental extraction
- Can't identify which of the 40 methods belong on the Order entity itself
- Doesn't notice that Order itself has become a near-empty data class