BLOG | Article

Coupling Consumer-driven Services: Understanding Patterns and Anti-patterns

Organizations that fully embrace microservices see them not just as technical tools but also as socio-technical tools. The design of software systems influences the communication paths and coupling between development teams.

Socio-technical: Systems that include technical systems but also operational processes and people who use and interact with the technical system. Socio-technical systems are governed by organizational policies and rules.

As microservice systems expand, the likelihood of socio-technical coupling rises substantially. This leads to the emergence of layers, which means that implementing new features for customers demands modifications that affect multiple layers of the architecture. Consequently, the challenge of coordinating several teams arises, each with its own backlog and performance objectives to meet.

Recognizing the patterns that exist within layered socio-technical architectures can save you from facing difficulties and conflicts in the future.

According to Wikipedia (ah Wikipedia), a Microservice: In software engineering, a microservice architecture is a variant of the service-oriented architecture structural style. It is an architectural pattern that arranges an application as a collection of loosely coupled, fine-grained services, communicating through lightweight protocols.

In this article, we will explore the concept of coupling that arises when services depend on each other, and the teams that build these services. We’ll discuss patterns that should be avoided and offer solutions to mitigate coupling. Additionally, we’ll identify potential hazards early on to prevent them from becoming major issues.

Subservient Contexts: A Common Anti-pattern to Watch Out for

As you delve deeper into the link between software systems and the organizations that create them, you’ll be amazed by what you discover.

It’s common to find dominant teams within an organization, led by assertive managers who push for their work to take priority and the software systems to be tailored to their needs. I’ve personally experienced this. As a result, there are also weaker teams (subservient teams) who have less power or assertiveness and must accept the decisions of the stronger teams, even if it’s not the best choice for the organization.

One can reflect the organizational pattern in the software architecture, emphasizing the socio-technical aspect of the systems. This results in Subservient Contexts, which ultimately leads to a bounded context. However, it is important to note that bounded contexts are not microservices.

Subservient contexts are created to meet the requirements of their consumers, either forcefully or as an attempt to be helpful. While it is admirable to go above and beyond to please stakeholders, it can result in dangerous socio-technical side effects due to compromises in software architecture.

It may seem like the weaker team is to blame for the subservient context, but in certain organizations, it could actually be the stronger team seeking more control and influence. In such cases, they may be willing to compromise on the architecture to meet their own needs.

Consumer-specific Endpoints

It is important for microservices to be able to develop independently, which is achieved through loose coupling — breaking up the architecture into smaller pieces. However, having personalized logic for a single-use purpose by a single consumer, typically exposed as an API endpoint, can undermine this benefit. This is because consumer-specific endpoints mean that new clients may have to connect to an endpoint designed for another consumer, which may not suit their needs and force them to compromise their design.

The solution to this problem is to add another consumer-specific endpoint for the new client. However, as the number of consumer-specific endpoints increases, the subservient context team may encounter problems. Their code complexity will increase as they have to support multiple use cases, and they may become a bottleneck as multiple consumer teams will be pushing them to make changes to their consumer-specific endpoints.

Moreover, if a new feature is added to one endpoint, it may also need to be added to other endpoints as well. The costs of such work can be double or triple, depending on how many times the feature has to be duplicated, and the maintenance burden will become equally costly.

An example of Consumer-specific Endpoints in the Bank

While working at a bank, I was involved in creating an integration system that would connect and share data between different teams. One interesting case we encountered was developing consumer-specific endpoints for a brand-new reporting system that would integrate various departments within the bank.

Initially, we had one core model for each domain that the reporting system required, but the schemas were not compatible. To make them compatible, the team had to build new endpoints based on the schema from the new system.

We could have avoided consumer-specific endpoints by moving the schema mapping to the new system or providing a client library for it, but this would have duplicated the change in the other system and introduced the risk of anemic domains and envious consumers.

It’s always interesting to encounter new problems and find solutions to them.

Anemic Domains and Envious Consumers (Anti-patterns)

Have you heard of the Anemic Domain Model and Feature Envy anti-patterns? They can occur in both Domain-Driven Design and distributed systems, causing issues with system design and maintenance.

One solution to avoid dependencies between microservices is to have the consumer take ownership of the necessary rules and data from downstream services. This eliminates the need for dependency but can result in the consumer being envious of features that should belong elsewhere.

As a result, the downstream service may be anemic and lacking in domain rules.

Repeated Domain Rules in BFFs and Frontends

It’s not uncommon to come across envious consumers who replicate the same domain rules in their front-end or Backend for Frontend (BFF) applications. Front-end teams tend to take the initiative to add their own business rules to their BFF applications without waiting for the domain teams to make changes. This is done to avoid dependency on APIs, and it’s usually done by web, Android, and iOS teams.

While it may seem harmless to allow three front-end teams to make changes to rules without being blocked by API teams, there are some drawbacks to this approach. For instance, if changes need to be made to the rules in the future, all three teams will need to make the same changes. This can lead to duplication of effort, which may result in subtle differences, incorrect changes, or changes made at different times. As a result, the user experience may vary across apps, and bugs may arise.

Fat Facade Contexts (Anti-pattern)

In a microservice architecture, there may come a time when we need to create abstraction. This happens when multiple consumers are calling multiple downstream microservices in the same sequence. The repetition of coordination logic in this scenario indicates the need for abstraction.

A facade microservice is a microservice that wraps around the coordination of other microservices and presents a simplified interface. This can simplify the interaction between consumers and the microservices.

However, there is a risk of creating a bulky facade. This occurs when a single microservice takes on multiple responsibilities that should be handled by separate microservices. This can make the microservice more complex and challenging to maintain.

It may seem like the weaker team is to blame for the subservient context, but in certain organizations, it could actually be the stronger team seeking more control and influence. In such cases, they may be willing to compromise on the architecture to meet their own needs.

When we combine multiple tasks into one microservice, we lose the advantages of having modular components. This results in an increase in sociotechnical coupling and a decrease in the ability to make changes to different parts of the system at the same time.

To address this, we should model the domain as bounded contexts and separate the coordination logic into its own facade context. Although it may result in additional service-to-service integration, this is the cost of implementing microservices.

The difference between BFFs and Facade Microservices

Designing where to put coordination logic can be tricky. Often, we lack necessary information to make a good decision upfront.

I think it’s possible that coordination logic might be a valid domain concept. If that’s the case, it would be important for all downstream users. The logic would likely be consistent regardless of who is making the request. It’s probably a good idea to have this logic housed in a microservice that can be accessed by many different consumers.

When it comes to coordination logic, it’s important to remember that each consumer may have their own specific needs. Each application may have its own unique way of displaying information on various web pages or app pages, but this is more of an app-specific design concern specific to the app rather than a domain concern. To ensure optimal performance, it’s recommended to keep this logic near the frontend in a BFF.

When it comes to designing microservices, choosing the wrong approach can lead to serious consequences. For instance, your microservices may end up being too closely tied to their consumers, or your domain may be lacking in substance. It can be difficult to determine which of these problems is at play until you have multiple consumers using your system. This is why it is so important to take the time to thoroughly understand the domain you are working in. By identifying key domain concepts, you can gain a clearer understanding of what belongs in your domain and what doesn’t.

Building Trust

At first, designing microservices may seem simple. However, as the system and teams expand, and dependencies increase, poor design choices can have major sociotechnical consequences. It becomes challenging to correct these issues, as they are built upon sub-optimal design layers.

To avoid these problems, it is crucial to consider how consumers impact the design of microservices. Dependencies and misplaced responsibilities can be easily introduced, costly to endure, and difficult to reverse. Understanding the fundamental patterns, such as facade contexts, and BFFs, can also help in designing microservices effectively.

On the other hand, it is essential to be aware of anti-patterns, including fat facades, subservient contexts, anemic domains, and consumer-coupled endpoints, to avoid making mistakes in microservices design.

Designing sociotechnical systems is a fundamental aspect of Domain-Driven Design, and learning DDD can prove to be beneficial in this area.






Thank you!