In a recent project I’ve had to implement a feature I think is a good example of a separation of concerns at a small scale.

This feature involves giving the user a choice from a set of items. The available items from which the user may select an option is determined by a couple of factors. First there is a pool from which they may draw items. Users will preferentially get only those items directly associated with themselves if any are available. If no items are associated with the user then they get those associated with their user class. Finally, if there are no items associated with either the user or the user’s class they get a set of unassociated items as their pool.

Once we have the pool we also do filtering on a set of dynamic criteria, such as the current order value. This filtered set them becomes the list of options the end user is presented with.

For various implementation reasons determining the pool items can be expensive but the pool items appropriate to a user change relatively infrequently. They are therefore perfect candidates for caching. The filtering of the pool is based on dynamic criteria that may change frequently and are therefore not amenable to caching.

What this leads to is that we have three separate concerns:

  • Retrievable of the pool items
  • Caching of the pool items
  • Filtering of the pool items

It would be possible to implement all of these concerns within a single service. That would isolate the client of the service from all of these concerns. It would pass the service the current relevant state (such as order value) and receive in response a list of items to display. That the service may use caching to reduce the impact of potentially expensive operations is irrelevant to the client, as are the exact rules used for filtering. The client is therefore simpler than if it new of these concerns.

Although the client may be simpler the service itself is not. It contains three concerns and must deal with all their interactions and implementation concerns. This does not conform to the Single Responsibility Principal.

We can simplify this by making each concern its own distinct service. At the lowest level we have a service that performs only the retrieval of the pool items. This service becomes the only point where we need to consider the current user, as its results will always be restricted to those available to that user.

On top of the retrieval service we can place a caching service. When items are requested from this service it will preferentially return the cached set if they exist. Otherwise it will utilise the retrieval service, saving the results object in the cache as well as returning them it its own caller.

Finally we have a filter service. It gets an item pool from the caching service and applies filtering criteria, returning the results to its own caller. It would therefore have the same signature as the original merged service.

This would seem to have tripled the complexity. Instead of a single service we now have three. However each individual service is significantly simpler than the original implementation. Each has a smaller area of responsibility and interacts with fewer external dependencies. Therefore what we have is lower overall complexity where we trade some increase in complexity between elements for lower complexity inside the elements. As we can use autowiring Dependency Injection containers to do all the hookup for us this is in most cases a significant gain in maintainability.

We additionally get reuse potential that would not otherwise exist. In this case we may be able to genericise the caching service to be applicable in many scenarios where caching is required. This would allow us to apply caching using interception to a variety of actions with only some configuration of the DI container.

Another area where benefits can be found is in future extension. Once these services were implemented a further requirement changed how the pool was generated. A new class of item was introduced that need to be handled separately. The same filtering sequence against user and user class were to be used, but we needed to do this separately for items of the class and items not of the class. The two resulting lists would then be merged.

As the interface of the retrieval service did not change it was possible to constrain the changes purely to this service. This localisation of changes simplified testing significantly.