Aggregation Services (The Knot)
Last updated
Last updated
Aggregation services main responsibility is to expose one single point of contact between the core business logic layer and any exposure layers. It ensures, if multiple services of any variation share the same contract, that they are aggregated and exposed to one exposer component through one logical layer.
Aggregation services don't hold any business logic in themselves. They are simply a knot that ties together multiple services of any number. They can have any layer of services as dependencies and it mainly exposes the call to these services accordingly. Here's a code example of an aggregation service:
As the snippet shows above, an Aggregation service may have any number of calls in any order without limitation. And there may be occasions where you may or may not need to return a value to your exposure layers depending on the overall flow and architecture, which we will discuss shortly in this chapter. But more importantly Aggregation services should not be mistaken for an orchestration service or any of its variants.
Aggregation services always sit on the other end of a core business logic layer. They are the last point of contact between exposure layers and logic layers. here's a visualization of where aggregation services would be located in an overall architecture:
Let's discuss the characteristics of Aggregation services.
Aggregation services mainly exist when there are multiple services, sharing the same contract or sharing primitive types of the same contract, that require a single point of exposure. They mainly exist in hyper-complex applications where multiple services (usually orchestration or higher but can be lower) require one single point of contact through exposure layers. Let's talk in detail about the main characteristics for Aggregation services.
Unlike any other service, Aggregation services can have any number of dependencies as long as these services are of the same variation. For instance, an Aggregation service cannot aggregate between an Orchestration service and a Coordination service. It's a partial Florance-Like pattern where services have to be of the same variation but not necessary limited by the number.
The reason for the lack of limitation of the dependencies for Aggregation services is because the service itself doesn't perform any level of business logic between these services. It doesn't care what these services do or require. It only focuses on exposing these services regardless of what was called before or after them.
Here's what an Aggregation service test would look like:
As you can see above, we are only verifying and testing for the aggregation aspect of calling these services. No return type required in this scenario but there might be in the scenarios of pass-through which we will be discussing shortly.
An implementation of the above test would be as follows:
By definition, Aggregation services are naturally required to call several dependencies with no limitation. The order of calling these dependencies is also not a concern or a responsibility for Aggregation services. That's simply because the call-order verification is considered a core business logic. which falls outside of the responsibilities of an Aggregation service. That of course includes both natural order of verification or enforced order of verification as we explained in section 2.3.3.0.1 in the previous chapter.
It's a violation of The Standard to attempt using simple techniques like a mock sequence in testing an Aggregation service. These responsibilities are more likely to fall on the next lower layer of an Aggregation service for any orchestration-like service. It is also a violation to verify reliance on the return value of one service call to initiate a call to the next.
Aggregation services are still required to validate whether the incoming data is higher-level structurally valid or not. For instance, an Aggregation service that takes a Student
object as an input parameter will validate only if the student
is null
or not. But that's where it all stops.
There may be an occasion where a dependency requires a property of an input parameter to be passed in, in which case it is also permitted to validate that property value structurally. For instance, if a downstream dependency requires a student name to be passed in. An Aggregation service is still going to be required to validate if the Name
is null
, empty or just whitespace.
Aggregation services are not also required to implement their aggregation by performing multiple calls from one method. They can also aggregate by offering a pass-through methods for multiple services. For instance, assume we have studentCoordinationService
and studentRecordsService
and anyOtherStudentRelatedCoordinationService
and each one of these services are independent in terms of business flow. So an aggregation here is only at the level of exposure but not necessarily the level of execution.
Here's a code example:
As you can see above, each service is using the Aggregation service as a pass-through. There's no need in this scenario whatsoever for an aggregated routines call. This would still be a very valid scenario for Aggregation services.
It is important to mention here that Aggregation services are optional. Unlike foundation services, Aggregation services may or may not exist in any architecture. Aggregation services are there to solve a problem with abstraction. This problem may or may not exist based on whether the architecture requires a single point of exposure at the border of the core business logic layer or not. This single responsibility of Aggregation services makes it much simpler to implement its task and perform its function easily. Aggregation services being optional is more likely to be than any other lower-level services. Even in the most complex of applications out there.
If an aggregation service has to make two different calls from the same dependency amongst other calls, It is recommended to aggregate for every dependency routine. But that's only from code-cleanliness perspective and it doesn't necessarily impact the architecture or the end-result in any way.
here's an example:
This organizational action doesn't warrant any kind of change in terms of testing or end-result as previously mentioned.
The most important rule/characteristic of an Aggregation service is that its dependencies (unlike orchestration services) must share the same contract. The input parameter for a public routine in any Aggregation service must be the same for all its dependencies. There may be occasions where a dependency may require a student id instead of of the entire student that is permitted with caution as long as the partial contract isn't a return type of another call within the same routine.
Aggregation services main responsibility is to offer a single point of contact between exposer components and the rest of the core business logic. But in essence, abstraction is the true value Aggregation services offer to ensure any business component as a whole is pluggable into any system regardless of the style of exposure this very system may need.
Let's talk about these responsibilities in detail.
An aggregation service performs into responsibility successfully when its clients or consumers have no idea what lies beyond the lines of its implementation. An Aggregation service could be combining 10 different services and exposes a single routine in a fire-n-forget scenario.
But even in pass-through scenarios, Aggregation services abstract away any identification of the underlying dependency from exposers at all costs. It doesn't always happen especially in terms of localized exceptions but close enough to make the integration seem as if it is with one single service that's offering all the options natively.
Aggregation services are also similar to orchestration-like services in terms of mapping and aggregating exceptions from downstream dependencies. For instance, if studentCoordinationService
is throwing StudentCoordinationValidationException
an Aggregation service would map that into StudentAggregationDependencyValidationException
. This falls back into the concept of exception unwrapping then wrapping of localized exceptions which we spoke about in detail in section 2.3.3.0.2 of this Standard.