Service Classes

What are services?

Services are the classes containing domain and application logic. Every service provides an API to trigger some actions, be it something small like formatting data or something big like creating orders.

Services are also notable because they don’t care what calls them. Each service encapsulates an operation that can be run by a controller, console command, test, or another service.

How to make good services

We want to make good services. But, to achieve this, we first need to understand what a good service means. A good service is:

  • Easy to use.
  • Easy to test.
  • Easy to modify when requirements change.

Another distinctive feature of good service is predictability. We should be able to tell what a service does just by looking at its external API and without knowing how the service works inside.

It should be true predictability. Sometimes the service API looks simple but doesn’t show the whole picture. The service might also do something else or work only in specific cases. For example, when we call a service to format a string, we’ll be unpleasantly surprised if we later find out that it works only on Fridays and also wipes the database. The primary goal of structuring code is to reduce the number of things we have to remember, so we should avoid false predictability.

Here are some things that can improve our services.

Follow SOLID principles

The first and foremost method is to remember and follow the SOLID principles. Make the granular services that implement granular interfaces and follow their intended logic. Make them depend on interfaces instead of implementations, and make them extensible via configuration.

Use Dependency Injection

If a service depends on other services, it has to accept dependencies as input in the constructor. This lets us influence the behavior of the service from outside. For example, we might want different dependencies for the service in production and testing. Dependency injections also help us reuse the existing service classes.

Avoid state

The only state stored in the service should be an immutable configuration accepted in the constructor. All the input information should be in the method input arguments.

Any non-configuration state in the service will lead to temporal dependencies between service methods. These are nasty surprises, like when the method doesn’t work as expected because we forgot to call another method before it. Things like that are hard to document and use.

The most prominent form of state in the services is when we have to initialize the service before using it. Let’s imagine a CSVExporter with this implementation:1

Get hands-on with 1200+ tech skills courses.