One of the things I always mention when I rant about misusing events is behavior mutation. Since listeners can be attached and sometimes even detached dynamically during application runtime it frequently results in event hell and severely impedes debugging. After some thought I believe same reasoning can be applied to services too.

In a nutshell technically a service is pretty much any class that is being instanced by some DI mechanism. From an architectural point of view it provides a facade to some part of the systems functionality. Since an instance of a service is the same for all classes using it a misuse of it may make parts of the system transiently dependent on each other.

Let’s use a router as an example:

class Router
{
    public function route($request)
    {
        //returns some sort of Callable
    }

    public function addRoute(Route $route)
    {
        //adds a new route
    }
}

Parts of the system that rely on using the route() method are transiently dependent on those that add routes to it. Meaning you can never be 100% sure that calling route() with the same request twice will return the same result. So when debugging you also have to take int account not just the service itself but also all the places it could have been modified in.

To avoid this the best approach is to write only immutable services, meaning that a service should not have methods that would modify its behavior. Our routing example could be rewritten as follows:

class Router
{
    public function __construct(array $routes)
    {
        
    }
    
    public function route($request)
    {
        //returns some sort of Callable
    }
}

Such an approach still allows for a pluggable architecture. The only difference is that it forces you to do plugin initialization before the service is actually built. A good approach for this is to use the Facade pattern by building the Routing subsystem separately and then building a limited Router service to it.

A good example of this is Doctrines EntityManager which is the Doctrine service that is used the most. It provides a limited functionality and limits the user from doing some crazy stuff like defining new Entities on the fly.