Creating better services

When writing code, you generally want to split up the logic in to different classes. You have your controller classes which take a request, and return a response. You write value objects to represent information which is important to your application. You may write commands, command handlers, repositories and more. The most important group of classes you write are probably your services, which hold (most) of the logic of your app.

There are a few rules which will help you write better, more reliable services, so lets go over them.

Services dont need data classes to be created

A service should not need ‘data’ in order to be created. For example, in an e-commerce setting, you may have a service class which deals with the logic surrounding the cart. This class can never have the current cart as a property, or require it in the constructor.

Of course you can still give the cart as an argument to a method, and perform logic on that. But the class itself should not hold the cart.

Requiring the cart would mean you need to do a lot of operations, just to create this class. You would need to know the ‘current’ cart id, the DB would need to be queried etc. This means the class becomes a lot harder to create.

Services dont have mutable state

A service should not have a data class as a property, or any data that can be mutated. For example, if you have a class which may behave differently in a test environment. Then whether or not this is a test environment should be injected in the constructor. You should not have a setter for this behavior.

When you allow the state of the service to be changed, then the service may have different behavior every single time, and it becomes unpredictable what happens.

An exception here is if you have a cache layer in your service. e.g. an array holding previously calculated results.

Services dont depend on a context class

A ‘context’ class is a class that knows about the current context. This may be a Request object, or an InputInterface for the CLI. Other examples are the symfony Security class, which knows about the current user. Instead the data of the current context that this service needs should be passed along with in a parameter of the method.

Doing so will allow you to use the service in a different context. For example a class that requires the InputInterface object is not usable in a web environment. And if you give the Security class to a service you become unable to ‘use’ another user, for example in the CLI.

Every method should do only 1 thing

This is true for almost all classes, but is worth stating again for services.

Instead of a method doing logging, querying the database and handling logic, let your service do exactly one thing. Move the database stuff to a repository, and handle the logging with a Decorator.

By splitting up the logic, your methods become smaller, easier to reason about, and easier to test.

Avatar
Gert de Pagter
Software Engineer

My interests include software development, math and magic.