kras99 - Fotolia
The vicious cycle of circular dependencies in microservices
Modular design is impossible so long as circular dependencies lurk in your architecture. So how can you break problematic coupling between otherwise-independent components?
Complex architectures like microservices lead to complex dependency relationships among applications, services and software components. Unless architects and developers have a clear dependency management strategy in place, it can be harrowing to locate problematic couplings before they lead to breaking changes and outages.
This tip explores strategies that developers can use to track and control microservices dependencies. We'll start with a quick explanation of the dependency challenge in distributed architectures. Then we'll detail how to manage problematic dependency cycles, including how things like graphs, maps, dependency tracking tools and remote procedure call authorization can help.
The modular design dependency problem
In software engineering, modularity refers to the degree to which an application can be divided into independent, interchangeable modules that work together to form a single functioning item that can serve a specific business function. Modularity promotes reusability, better maintenance and manageability and promotes low coupling and high cohesion.
Despite the benefits it offers, modular design is still plagued by dependency problems. In a typical microservices architecture, you'll often encounter dependencies among the services and components. Although these services are modeled as isolated, independent units, they still need to communicate for the purpose of data and information exchange.
Ideally, a microservices application shouldn't contain circular dependencies. This means that one service should not call another one directly. Instead, those services should operate on event-based triggers. However, reality dictates that most developers will still need to closely link certain parts of an application, and problematic dependencies will persist.
What are circular dependencies?
A circular dependency is defined as a relationship between two or more application modules that are codependent. Circular dependencies in a microservices-based application can hurt the ability of services to scale or independently deploy, as well as violate the Circular Dependencies Principle. When the abstractions exhibit circular dependencies, developers must update, test and reuse them simultaneously in response to each change.
Circular dependencies also make code difficult to read and maintain over time, which opens the door to error-prone applications that are difficult to test. If circular dependencies proliferate an architecture, any changes to a single module will likely cause a large ripple effect of errors for others. It's also extremely difficult to reuse tightly-coupled modules since their code carries all their dependencies.
In short, excessive intermodule dependencies reflect poor software architecture design.
Reordering dependencies with abstraction
To reduce or eliminate circular dependencies, architects must implement loose component coupling and isolate failures. One approach is to use abstraction to break the dependency chain. To do this, you introduce an abstracted service interface that delivers underlying functionality without direct component coupling.
Let's explore this idea in more detail. Figure 1 illustrates two classes -- X and Y -- that are caught in a mutual dependency cycle. Class X depends on Class Y, and vice versa.
To break this dependency problem, we'll extract the functionality of Class X into a new, abstracted interface -- Interface I -- that contains and delivers Class X's functionality, as illustrated in Figure 2. Now, Class Y can rely on the interface rather than its coupling with Class X. In turn, Class X can retain its dependency with Class Y in a nonrecursive manner by extending the functionality to the interface.
Adopt the Dependency Structure Matrix
The Dependency Structure Matrix (DSM) is a simple and compact way of mapping the dependencies that exist among the components of an application. This visual representation of the architecture's coupling can help software teams navigate testing and quickly detect dependency-induced failures.
The rows and columns of a DSM represent components that are dependent on one another in some way. Cells are either left blank or contain a mark. If the cell isn't marked, it implies that there is no significant dependency between those components. If a cell contains a mark, it indicates a significant dependency that could potentially pose a risk to the system's resiliency. For instance, you may want to examine the dependency relationships between a certain software task and a particular application module. You can apply the DSM approach to do so, as shown in Figure 3:
Tools to fix problem dependencies
The right tooling is extremely helpful when it comes to addressing circular dependencies among microservices, and arguably essential when dealing with large, distributed architectures. Here are some examples of tools designed to manage dependencies in microservices applications:
- Lattix Architect provides a comprehensive visual map of the application's architecture using the DSM to identify problematic dependencies. It also provides support for dependency testing and any necessary code refactoring.
- Stan4J is a popular tool for both Windows and Mac OS development, especially applications built in Java. It features simplified integration into existing development environments, detects problem dependencies embedded in code and provides visual dependency analysis to simplify planning.
- Structure101 is designed for C, C++ and Java. It provides support for visualization of dependency features and can review relationships to identify the affect a change has on associated services. Structure101 also provides methods to analyze an architecture's overall resiliency.