Matthias Noback's Advanced Web Application Architecture (AWAA from here on) is excellent throughout. You should buy it and heed its advice. It is a wonderful companion or followup to my own Modernizing Legacy Applications in PHP -- which is still free, though of course I'll happily take your money.
Having read through the book, I paid special attention to the sections on Application Services. The pattern is a good one, and you should use it, but I find I must offer an alternative approach to the AWAA treatment of the pattern.
tl;dr:
- Each use case should have its own Application Service
- Treat each Application Service as a Facade, not a Command
- Each Application Service should return a Domain Payload
- Only the Presentation should call the Application Service
One Use Case Per Application Service
AWAA suggests that an Application Service may cover either single or multiple use cases.
An application service could have a single method (like the __invoke()
method). In that case the service represents a single use case as well. But since several uses cases may share the same set of dependencies you may also create a single class with multiple methods. (pp 255-256)
While it is true that some form of efficiency is gained when use cases "share the same set of dependencies," I think that the gains are illusory.
Instead, I say an Application Service should cover one-and-only-one use case:
-
One Application Service per use case makes the available use cases more visible when browsing through the directory structure. It becomes trivial to see what is supported by the system, and what may not be.
-
Doing so sets up a standard practice within the system, which in turn helps to avoid questions like "Should I put this use case in an existing Application Service, or should I create a separate one?" With this practice in place, there's no need to think about it: always create a separate one.
-
It reduces merge/rebase conflicts when multiple developers are working in the Application Service code. For example, if three developers are working on three separate use cases inside a single Application Service, the chances for conflict are increased. If the use cases are each in separate Application Services, the chances for conflict are reduced.
-
While most use cases may start out simple, some will become more complex, and it is difficult to predict which ones. As a use case becomes more complex, you already have a separate "space" to work in if it is in separate Application Service. The need for extracting and creating a new Application Service is pre-empted when each use case already has its own Application Service.
-
Finally, a single use case Application Service maps very nicely to a single ADR Action, or to a single-action MVC controller. It makes dependency injection much more specific and targeted.
Application Service as Command?
AWAA says Application Service methods are Command methods:
Application service methods are command methods: they change entity state and shouldn’t return anything. (p 257)
But then it goes on to say ...
However, when an application service creates a new entity, you may still return the ID of the new entity. (p 257)
That way, the code that called the Application Service can "use the ID that was returned by the application service to fetch a view model from its view model repository." (p 257)
Here is an example of what the book means; these examples are mine, and not from AWAA. First, the Presentation code, e.g. from a controller action method:
$orderId = $this->createOrderService->__invoke(/* ... */);
$order = $this->orderViewRepository->getById($orderId);
/* ... present the $order in a Response ... */
And the relevant Application Service code:
$order = new Order(/* ... */);
$order = $this->orderWriteRepository->save($order);
return $order->id;
However, I disagree that an Application Service ought to be treated in general as a Command.
While some Application Services might be Commands, the fact that sometimes the Application Service should return something indicates that they do not as a whole follow the Command pattern. It is true that a Command may "publish events" through an event system, but a Command is never supposed to "return" anything.
Application Service as Facade
Instead, I suggest treating the Application Service as a Facade.
That is, an Application Service should be seen as a Facade over the underlying domain operations. A Facade is allowed to return a result. In the case of an Application Service, it should return a result suitable for Presentation.
Modifying the above example, the code becomes something more like the following. First, the Presentation code:
$order = $this->createOrderService->__invoke(/* ... */);
/* present the $order in a Response */
And the relevant Application Service code:
$order = new Order(/* ... */);
$this->orderWriteRepository->save($order);
return $this->orderViewRepository->getById($order->id);
The details may be up for discussion, but the larger point stands: treating the Application Service as a Facade, not a Command, creates a more consistent and predictable idiom, one that actually adheres to the stated pattern.
Payload
Once we treat the Application Service as a Facade instead of a Command, we might ask what it should return. In the above simple cases, the Application Service returns a View Model (per AWAA). However, that presumes the operation was a success. What if the operation fails, whether due to invalid input, raised errors, or uncaught exceptions, or some other kind of failure?
My opinion is that the Presentation code should not have to handle any errors raised or exceptions thrown from an Application Service. If the Presentation code has to catch
an exception that has come up through the Application Service, and then figure out what those errors mean, the Presentation code is doing too much -- or at least doing work outside its proper scope.
As a solution for that constraint, I have found that wrapping the Application Service result in a Domain Payload is very effective at handling both success and failure conditions. This allows a consistent return type from each Application Service in the system, and allows the Presentation code to standardize how it presents both success and failure.
For example, the above Presentation code can be modified like so (note the use of an ADR Responder to handle the Response-building work):
$payload = $this->createOrderService->__invoke(/* ... */);
return $this->responder->__invoke($payload);
Likewise, the relevant Application Service code:
try {
$order = new Order(/* ... */);
$this->orderWriteRepository->save($order);
$result = $this->orderViewRepository->getById($order->id);
return new Payload(Status::CREATED, order: $result);
} catch (Throwable $e) {
return new Payload(Status::ERROR, error: $e);
}
The inclusion of a Domain Status constant lets the Presentation code know exactly what happened in the Application Service. This means the Presentation code does not have to figure out what the results mean; the Payload tells the Presentation code what the results mean via the Status.
Events and Delegation
Regarding event subscribers and eventual consistency, AWAA advises:
Instead of making the change inside the event subscriber, delegate the call to an application service. (p 270)
If the Application Service actually is a Command, as AWAA suggests, this may be fine.
However, when using a Payload as the return from each Application Service, no Application or Domain activity should delegate to any other Application Service. The Payload is always-and-only to be consumed by the Presentation code, never by any other kind code.
I suggest that if you find you need to use the logic from one Application Service anywhere else, and you are using Payload returns, you should extract that logic from the Application Service and put it into the Domain where it belongs. Then event subscribers can call the relevant Domain element, instead of an Application Service.
Conclusion
These alternative offerings should in no way discourage you from buying and reading Advanced Web Application Architecture by Matthias Noback. It is a very good piece of work with a lot to offer. My suggestions here are more an alternative or refinement than a refutation or rejection. As a developer, you should consider Noback's advice as well as the advice presented here, and determine for yourself which is more appropriate to your circumstances.