Microservices Modeling— Common Pitfalls — PART I — Single Responsibility Principle
Part I: Intro
Microservices are an architectural pattern that allows big software systems to be break in smaller parts. Each microservice can be developed, maintained, deployed and operated independently.
Microservices share the same general principles of any modular architecture: break the system into smaller, independent, loosely coupled modules that communicate using a well defined interface (API).
Microservices aim to solve the organisational challenge of having many people contributing to the same system, allowing parts that evolve independently, to be developed and deployed independently. Creating different modules and having the ability to compose them into bigger and more complicated systems is something core to any development stack. Any technology supports a form of module packaging and publication. Microservices take the module approach one step further. Instead of bundling all modules in the same deliverable, with microservices each module becomes a unit of deploy, execution and operation.
When adopting microservices architectures we need to deal with two types of challenges: the challenges that microservices bring as they come as independent units of deployment AND the fundamental challenges of building any modular system.
Now the (not so) funny thing is this: Building a modular system is something very hard to be done right. A microservices based architecture is a harder way of implementing a modular architecture. Adopting a microservices based architecture brings two types of challenges. The first is that any mistake done in the modular architecture will be more challenging to fix. The second are the new class of problems that microservices bring.
The second class of problems have to do with how to operate a system, made of many distributed parts, that collaborate together to create a higher level unit. These problems include: Testing strategies, independent delivery pipelines, more infra-structure, observability (monitoring/alarmistic), distributed tracing, failover, fault tolerance patterns, versioning/evolvability, etc.
The first class of problems are the general challenges of writing good software, but with a catch: microservices amplify most of the errors we do in the design of the system. I’ll list the most common design flaws I’ve seen when designing these systems and discuss better alternatives.
This article will focus on some design principles that are key to get a sustainable design. It will provide examples of the common pitfalls that happen by violating those principles and some approaches to deal with it in a microservices architecture.
Part II: Design Principles
Disclaimer: this section is written using many of the ideas from Robert C. Martin book Clean Architecture.
The SOLID principles guide us in how to create and arrange the bricks of our software buildings. While those principles are commonly used to describe the design of classes, it does not imply they cannot be applied in other higher level components. A class is nothing more than a group of data coupled with functionality. There are of course other types of groupings, like a (micro) service where these principles also applies. The goal of these principles is to create pieces of software that tolerate changes, are easy to understand and can be used as modules of many systems.
SRP: Single Responsibility Principle
“An active corollary to Conway’s law: The best structure for a software system is heavily influenced by the social structure of the organization that uses it so that each software module has one, and only one, reason to change.”
— Clean Architecture, Robert C. Martin
This might be the least well understood of all SOLID principles. That’s because of a poor naming choice. Programmers easily assume it means that every module should do just one thing. Although there is indeed a principle like that, it is not the SRP. The right description of SRP is this:
A module should have one, and only one, reason to change.
Software systems are changed to satisfy actors (users/stakeholders); those actors are the reason to change. Thus we can rephrase the principle to say this:
A module should be responsible to one, and only one, actor.
A module is a cohesive set of functions and data. Cohesion is the force that binds together the code responsible to a single actor.
By putting these two endpoints in the same module, there is a coupling between different actors. This coupling can cause changes for the Customers to affect the Sales department.
Suppose the order process will change to support a more complex workflow and will start to accept two new states, a “pending” state to support asynchronous payments and an “canceled” state to allow users to cancel a payment.
A developer takes the task to implement the new workflow and changes the database to include a new column with two new possible states “pending” and “canceled”. The developer creates the tests to validate the functionality and everything works fine, the system is deployed.
The Sales team didn’t know about the change. The sales reports continue to be generated based on the same query that ignores the existence of a new column that allows new states. Eventually the reports will contain canceled and pending orders, generating incorrect sales data.
This is a symptom we’ve all seen happen. It is caused because we put together code that different actors depend on. SRP is about separating the code for different actors.
The solution to this problem is to separate the function and the data into different modules. The Order Service has its own data and functions together. It now publishes a message when an order is completed. The OrderCompleted message becomes part of the public interface of the service and it is published only when an Order is completed. Notice that whatever implementation the business workflow can have, the OrderCompleted message is only emitted when an order is completed. The Orders Service team can now evolve the workflow as they need without the concern of breaking other modules as long as the public interfaces don’t change.
Another obvious benefit is that Sales team can now tweak the reporting database however they need. Both systems are free to evolve its own schemas, indexes, etc to better suit their specific needs because each module has now its own database.
One of the downsides of this solutions is that we have now two public APIs. If we keep adding services we will have a forest of front facing APIs. A common solution to this is to use a Facade.
Another downside of this solution is that now there are two different services with their own databases to maintain. This is the price to pay to take the autonomy of modules to the level of the services.
Of course there is a middle way solution between the first monolithic example and the service per module aka microservices solution. The order service and the sales reporting service can be developed as internal modules of the same deliverable service. The same service hosts both modules here. This is a modular monolith.
There are countless examples of monolithic systems that fail, not because they are monoliths, but because they lack a proper design. It is possible to develop modular monolithic systems that have many of the benefits that microservices architectures claim. Those benefits come mostly from a good usage of design principles that allow the system to evolve and be easy to understand.
A good heuristic to determine if a team has the maturity to jump into a microservices architecture is if they have proven competence in delivering good modular monolithic systems. If a team struggles with getting a modular architecture right with a monolith, they will probably get in more trouble when migrating to microservices.
The main benefit of microservices is that it allows to scale big organisations, allowing independent services that evolve at different paces to be developed and deployed independently.
Another arguable advantage of adopting a microservices architecture is how it naturally enforces the boundaries across different components. Having different code bases with independent deploys makes it virtually impossible to violate the public interfaces (public APIs) published by each service.
Enforcing boundaries among modules inside the same code base is more difficult. Developers have access to all the code base so they can easily reference a class that was not supposed to be referenced, change a method or class accessibility or exposing some functionality to the wrong component.
This is the end of the first part of a series about design principles applied to microservices architectures.
To be continued…