Solving Cross-Cutting Concerns through patterns

Guest contribution by | 30.11.2020

The idea of tackling recurring problems through similar solution patterns is probably as old as mankind itself. If you search Google for “solution patterns”, you will find contributions from areas such as controlling, social sciences and of course software development. We use solution patterns as models to find solutions for problems. We see a problem and consider whether it is similar to a problem that we have already solved. This results in models or simply patterns of how to approach certain problems.

In object-oriented programming at the beginning of the 90s of the last century, so-called Design Patterns emerged which offered solutions to recurring problems, such as the decoupling of different implementations. As a rule, these problems related to the arrangement and application of certain classes to each other and the organisation of their relationships1. The design patterns could be used again and again, as they had already proven themselves in different contexts. As a result, the quality of the implementation increased significantly.

In the modern world of microservices and cloud architectures it is no longer necessary to organise individual classes. But the organisation of individual services and their communication with each other is just as complex and requires corresponding model solutions. The problems that arise are often the same and therefore the application of solution patterns is obvious. To distinguish these solution patterns from the original design patterns, they are called architecture patterns.

Such architectural patterns become particularly interesting when they relate to cross-cutting concerns. Cross-cutting concerns affect architectures again and again and affect all services that are needed in an application. Especially non-functional requirements like

  • reduction of overall complexity,
  • resilience,
  • security and
  • observability

are such cross-cutting concerns that are addressed by architectural patterns.

Reducing of Complexity

Mastering complexity is a recurring task in software development. In particular, the mastery of many services in a microservice architecture brings new challenges that should not be underestimated. One pattern for reducing this complexity is the sidecar. Here, just like a motorbike, a sidecar is an additional service attached to a docker container. Typical applications for such a pattern are:

  • web proxy,
  • logging-shipper and
  • master data clients.

Let’s have a look at two examples of sidecars:

Sidecar approach with Kubernetes

Kubernetes (K8s) is an open source system for automating the deployment, scaling and management of containerised applications2. In Kubernetes, the solution is quite simple, since the Kubernetes Pods provide an abstraction layer for containers anyway. This means that containers are always delivered within a Pod.

Sidecars have long been used in Kubernetes – it simply lends itself to this. However, it is difficult to control the lifetime of the individual containers without additional help. Therefore Kubernetes offers since version 1.18 an identification of a Pod as a sidecar. If containers are marked this way, they are started before all others and stopped after all others. In everything else they behave like normal containers3.

Sidecar pattern with Kubernetes

Figure 1 Sidecar pattern with Kubernetes

Example:

apiVersion: v1
kind: Pod
metadata:
  name: bookings-v1-b54bc7c9c-v42f6
  labels:
    app: demoapp
spec:ss
  containers:
  - name: bookings
    image: banzaicloud/allspark:0.1.1
    ...
  - name: istio-proxy
    image: docker.io/istio/proxyv2:1.4.3
    lifecycle:
      type: Sidecar
    ...

Figure 2 Example of a sidecar using Kubernetes

Sidecar in Docker-Compose

Sidecar structures can also be easily reproduced with Docker-Compose. Here, Compose is a tool for defining and running Docker applications with several containers. A YAML file can be used for configuration. With simple commands you can then create or start all services of the configuration4 .

Example:

services:
    reverseproxy:
        image: reverseproxy
        ports:
            - 8080:8080
            - 8081:8081
        restart: always
 
    nginx:
        depends_on:
            - reverseproxy
        image: nginx:alpine
        restart: always
 
    apache:
        depends_on:
            - reverseproxy
        image: httpd:alpine
        restart: always

Figure 3 Example of a reverse proxy as sidecar in Docker-Compose5

The sidecar pattern is mainly used to stabilise and reliably ensure the operating result of a running software. Prepared sidecar containers simplify and standardise typical tasks such as providing monitoring and logging. The whole operational complexity is simplified when using the sidecar pattern.

Resilience

Resilience is derived from the Latin resilere for bounce back, bounce off. In the case of technical systems, we understand it to mean that even partial failures do not cause complete failure. A pattern that sustainably supports this characteristic is the Ambassador6.

If services are located outside their own domain, they usually have no influence on the availability or response times of external services. The resilience of the consuming service is achieved by sending an ” Ambassador” to the consuming domain.

Ambassador pattern

Figure 4 Ambassador pattern

This pattern is particularly interesting for the distribution of master data. Many services access a central service with central data. Such a “single point of failure” can quickly lead to failures of the entire system.

The Ambassador is attached to the consuming service as a “sidecar” (see figure 1). It buffers the data and makes it available to the specialist application in a transparent manner. Transparent here means that the specialist application accesses the Ambassador as if it were directly accessing the master data service. The interfaces are the same. The specialist application only accesses the Ambassador and the data can be made available quickly and independently of the master data service. In order to be able to make changes available quickly and consistently, appropriate invalidation messages must be agreed upon. To become even more independent, the data can also be distributed via an event bus.

Event busses differ significantly from message busses, since producer and consumer are completely independent of messages. While a message bus forwards messages to the consumer, the messages are stored on the event bus. The messages stored in this way can be read by the consumer. This means that the consumer does not have to be available when messages arrive, as is the case with message busses.

Ambassador pattern with event bus

Figure 5 Ambassador pattern with event bus

Although such an event bus again looks like a single point of failure, the advantages outweigh the disadvantages. News producers do not have to rely on consumers being available. Corresponding “retry mechanisms” are not necessary. However, the advantages are bought by an additional element in the infrastructure.

Security

Security also needs to be re-evaluated in a microservice architecture. Since microservices usually provide REST interfaces, it must be prevented that they can be attacked by unauthorised persons. Here the pattern of an API gateway can help.

Principle of an API Gateway

Figure 6 Principle of an API gateway

The picture above shows the principle of an API gateway. In large microservice architectures, the number and behaviour of APIs (Application Programming Interface) quickly becomes confusing.

The APIs provided by microservices are often fine-grained – and not really what a consumer needs. The consumer has to interact with many services to get the information he needs. For example, in order to provide the user with a detailed description of a product, the client has to retrieve the description, photo and price of different services, each of which has to have an address.7

It therefore makes sense to combine APIs of microservices belonging to one domain via one access path. This aggregation is provided by an API gateway. This aggregation makes consumer creation and monitoring much easier, as only one entry point to the domain needs to be known. Via these gateways, access to the domain can then be secured and controlled by external services. Accesses can be monitored and if necessary rejected already here e.g. because of too high load. The API gateway also assumes the function of a gatekeeper, which only allows accesses that are expected and have been previously configured.

API gateways are offered by various cloud providers and software manufacturers. Examples include Apigee (Google)8 and Mulesoft9. These platforms offer much more than the actual API gateway itself, but cover entire API management tasks including publishing, testing and subscribing to APIs.

Observability

The observation of microservices plays a prominent role in keeping architectures stable and operational. In order to achieve higher observability, corresponding services can be assigned to each business service in the network of microservices. This leads to the pattern of the service network or service mesh.

Similar to the sidecar pattern, the service mesh also attempts to simplify the higher infrastructure complexities occurring in microservice architectures by defining typical installation patterns in advance. And similar to the sidecar pattern, a picture is used to illustrate the functionality. Mesh describes the grid of service to service communications in a microservice architecture.

A service mesh is a dedicated infrastructure layer to support service-to-service communication between microservices. The sidecar proxy is often used as a typical pattern.10 It then takes on the task of addressing the actual service or also addressing other services and also provides the functionalities for observation.

Principle of a service grid or service mesh

Figure 7 Principle of a service grid or service mesh

As with the sidecar, such dedicated layers offer advantages in terms of observability, secure connections, or even automatic repetition of erroneous calls.

These cross-cutting concerns no longer have to be solved individually in the application layer, e.g. by using libraries, but can be solved within the infrastructure in a standardised way for all microservices of the respective application.

There are different implementations for service meshes. The best known is probably Istio11. Further implementations are Consul12 and Kuma13.

Summary: Pattern for Cross-Cutting Concerns

In this paper four solution patterns for cross-cutting concerns of the software architecture are presented, such as reduction of complexity, resilience, security and observability.

These patterns provide solutions for recurring problems. We can solve these problems with the same or similar means and can already rely on corresponding experience. The four presented patterns do not only address the topics addressed, but support multiple non-functional requirements. In addition to the topics presented, topics such as performance and availability are also addressed.

Reduction of complexity14 Resilience Availability Performance Security Observability
Sidecar x x x
Service Mesh x x
API-Gateway x x x x
Ambassador x x x x

Table 1 Overview of presented patterns and related cross-sectional topics

The above table shows that the applicable patterns address specific non-functional requirements to be fulfilled. Solution patterns can be applied to better structure and simplify complex issues in the microservice world.

 

Notes (in German and English):

[1] E. Gamma, R. Helm et al.; 1994: Design Patterns. Elements of Reuseable Object-Oriented Software, Prentice Hall
[2] See https://kubernetes.io/de/, retrieved on 09.11.2020
[3] Sereg, Marton: Sidecar container lifecycle changes in Kubernetes 1.18, 404.02.2020
[4] NN: Overview Docker Compose, https://docs.docker.com/compose/, retrieved on 09.11.2020
[5] Hong, K.: Docker Compose: NGINX Reverse Proxy with multiple containers, https://www.bogotobogo.com/DevOps/Docker/Docker-Compose-Nginx-Reverse-Proxy-Multiple-Containers.php, retrieved on 17.05.2020
[6] NN: Botschaftermuster, https://learn.microsoft.com/en-us/azure/architecture/patterns/ambassador, 23.06.2017, retrieved on 08.11.2020
[7] Compare Richardson, C.: Pattern: API Gateway/ Backend for Frontend, https://microservices.io/patterns/apigateway.html, retrieved on 09.11.2020
[8] https://apigee.com/about/cp/api-management-platform, retrieved on 09.11.2020
[9] https://www.mulesoft.com/, retrieved on 09.11.2020
[10] NN: Service mesh, https://en.wikipedia.org/wiki/Service_mesh, retrieved on 09.11.2020
[11] https://istio.io/, retrieved on 09.11.2020
[12] https://www.consul.io/, retrieved on 09.11.2020
[13] https://kuma.io/, retrieved on 09.11.2020
[14] This refers to the complexity for the consuming service – not the overall complexity of the grid.

Dr. Annegret Junker has published additional articles in the t2informatik Blog, including:

t2informatik Blog: Reasonable limits for self-responsible teams

Reasonable limits for self-responsible teams

t2informatik Blog: Why I am not superfluous as a software architect

Why I am not superfluous as a software architect

t2informatik Blog: From Monolith to Microservices

From Monolith to Microservices

Dr. Annegret Junker
Dr. Annegret Junker

Dr Annegret Junker works as Chief Software Architect at codecentric AG. She has been working in the software industry for over 30 years in various roles and different domains such as automotive, insurance and financial services. She is particularly interested in DDD, microservices and everything related to them.