Don't name it Service!
Developers often fall into the trap of naming classes XService
— like FoodService
for food-related logic. Over time, FoodService
accumulates methods like add
, save
, get
, build
, getMore
, and even getXWithYWhileZ
. This pattern leads to bloated, unmaintainable code that violates core design principles and creates tightly coupled systems.
In this post, we’ll explore how to move away from this pattern using three strategies:
- Better naming
- Command-query separation
- Domain separation
The Problem with XService
It usually starts innocently: FoodService
has just a couple of related methods. But as new needs arise, developers keep adding more. Over time, this "junk drawer" service grows into an incoherent collection of logic.
This often leads to tight coupling. For example, YService
may need one method from XService
, so it takes a dependency on the whole class. If later, XService
also needs something from YService
, you’ve got a circular dependency. In many dependency injection setups, this might not be immediately obvious due to lazy loading — but it’s still there.
Testing also becomes a mess. You can't easily mock individual methods without creating a full mock of the entire XService
, including unrelated behavior.
1. Better Naming
Instead of lumping everything into FoodService
, consider splitting responsibilities into focused, intention-revealing classes:
AddFoodService
/FoodAdder
GetFoodService
/FoodGetter
BuildFoodService
/FoodBuilder
Even better, drop the “Service” suffix entirely and name classes by what they do: FoodAdder
, FoodRetriever
, etc. This makes their purpose clearer and helps avoid scope creep.
You can enforce naming conventions within your team (e.g., all database writers are called XAdder
). This consistency boosts understanding and onboarding.
2. Command-Query Separation (CQS)
A powerful architectural tool is command-query separation:
- Commands perform actions and have side effects but return no data.
- Queries fetch data but do not change system state.
This separation makes code easier to reason about:
- Queries: Safe to call, can hit the database directly, easy to cache and test.
- Commands: Focus solely on enforcing invariants and applying changes.
While a command might return a generated ID or status, it avoids reading system state unless absolutely necessary.
Example: Instead of having a
createAndReturnFood
method, you might haveCreateFoodCommandHandler
(writes) andGetFoodByIdQueryHandler
(reads).
3. Domain Separation
Another alternative is to keep logic within the domain model and reduce the role of services altogether.
For example, rather than having FoodService.doSomething()
, you might:
- Load or create a
Food
domain object. - Call a method like
food.updateNutritionalInfo()
that encapsulates the logic. - Persist the result via a simple repository or gateway.
This keeps logic close to the data it manipulates and separates orchestration from behavior.
Example: Split Use-Cases
Consider a Questionnaire
used in two different ways:
- One user creates it.
- Another user answers it.
These are fundamentally different workflows. Instead of a single QuestionnaireService
, split into:
QuestionnaireDesigner
QuestionnaireResponder
Trying to merge these into one service will create unclear responsibilities and maintenance headaches when either use-case evolves.
Conclusion
Instead of relying on bloated XService
classes, consider one or more of these improvements:
- Name classes for what they actually do, not just as generic services.
- Separate commands and queries to clarify intent and side effects.
- Push logic into the domain model and keep orchestration thin.
Combining these approaches leads to cleaner, more modular, and more maintainable systems — and makes your architecture easier to evolve as complexity grows.