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 have CreateFoodCommandHandler (writes) and GetFoodByIdQueryHandler (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:

  1. Load or create a Food domain object.
  2. Call a method like food.updateNutritionalInfo() that encapsulates the logic.
  3. 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.