Microservices can harbour perils and downsides that we would not have to face on simple monoliths. Circular dependencies between modules, can be the main danger.
Microservices are not the problem (nor the solution)
Microservices are sometimes perceived as a silver bullet and sometimes as a fashion statement. In reality, they are an architectural choice, a tool with both pros and cons.
Microservice are A solution
The main aim of microservices is to address the incremental deployment nature of modern software development.
They facilitate “fast and safe cycle times”, because they help in changing the System as little as possible, lowering the risk of a disaster, and simplifying rollback.
Microservices help when you CANNOT stop a production machine: it often turns out in fact, that you can indeed turn off one module, if that module is appropriately designed (as in separated).
NOTE this “perk” of microservices is also a constraint: by nature, a microservice should do as little as possible, and by that I mean it should “incarnate” something between an aggregate and a bounded context. This makes microservices difficult, because their correct bounding is about “what a microservice means” in the system, so it is about the context (the company) and the vision (the roadmap), rather than how many lines of code they should encompass.
Microservices are design tool
Microservices being a design and deployment tool can be such an obvious point, that we might overlook it, so let me highlight
- (Strong) Design tool: the instrument you use to trace lines. In our context, there is no more separating line than the one of the process as in operating system process: a microservice is a different executable as the very first thing.
- (Simple) Deployment tool: the instrument we use to bring something to production. In their “executable” nature, microservices are the simplest form.
Microservices are expensive $$$
The main cost of a microservices is “overhead”, and it comes in two flavours
- Execution overhead: being a separate process, it means that they have a start-up cost, a memory cost, and a processor cost: many of us also need to add the “runtime” cost. Those are many costs, and they compound a lot the more microservices you have.
- Boilerplate overhead: there is usually so much “non-business” code to write, that one needs more people to do all the work. Additionally, testing can get complicated if one does not have a strong vision and someone enforcing it.
- Complexity overhead: fragmented systems tend to become very complex quickly, prompting for ad-hoc solutions, for example:
- Distributed transactions: often “rolling back” or “canceling” a distributed transaction must be tackled from a domain perspective, which means tackle the issue with a custom logic (the sql “transactional” construct is not going to save you)
- Dependency hell: taming dependencies can be a challenge, when there are so many of them.
The more microservices you have, the more you have to feed the “overhead” beast. This is the reason why one might want to cheap-start with a monolith: at the beginning you might not have enough resources to scale.
Microservices have a high up-start cost.
Microservices are cheap $
Once the initial cost is faced, microservices start becoming cheap, because they are like “the pieces of a car”: we call this “low coupling(LCp)/high cohesion(HCh)” or “composability”. The argument is given in relation to these main categories:
- Cheap to think about, LCp: when a service is doing a single thing, it is much easier to manage and to assess: having multi-tools is a liability, because you might find that you need to blink your lights to break in your car, because it was “easier” for the manufacture to put the command there rather than in the pedal: who needs the blinker anyway…
- Cheap to change, HCh: when everything relative to a function is in the same place, it is much faster to navigate code, and there is only one place to change.
- Cheap failures, composability: unexpected malfunctions are often limited to a single component, especially if we use workflows.
- Cheap to compose, composability: if components do not share code, they then must use common protocols to communicate: for example http and rest. Note that the decoupling here is deep, as it allows for many parties to share the same growing pool of functions or resources.
Microservices are cost-effective, once in place
Our playground is not your sandbox1
A product part of a big enterprise infrastructure, is like a playground:
- It is a public space.
- Many people use its diverse services or simply pass by.
- It requires ongoing maintenance, such as:
- Restocking the bar.
- Fixing the benches.
- Adding ping pong tables and removing the cycling path.
IT personnel are often the playground’s maintainers. Occasionally, they prioritize their preference for landscape design over public utility. Landscaping is feasible only in “secret gardens” where hardly anyone, except designers, frequent; these are typically low-stake (unimportant) systems, also known as “sandboxes”.
Real playgrounds are high stakes, full of people with competing interests, for whom the facilities of the playground are so important that they cannot function without: caregivers NEED robust benches to wait for their kids, kids NEED a place to be loud and play, chess players NEED a calm place where they can play together, groups of friends of various species compete for the use of the greens; everyone needs the bathroom and the bar. People fall in love, bleed, relax, quarrel: all th the same time, possibly in the same playground.
This just to mention why we need teams of people taking care of the playground, and that shared responsibility means that we will take turns at who is cleaning the bathrooms. More importantly, it is interest of everyone (users and maintainers alike) not to have anyone destroy or jeopardize the public spaces.
Our public spaces should grow organically, adapting to the needs of their users, much like a well-maintained playground evolves over time. Unlike a literal playground, which might shrink due to overuse or neglect, our enterprise systems thrive when thoughtfully expanded. For instance, the bar might utilize the services of a nearby bank for convenience, illustrating how integrated services can enhance functionality.
However, it is crucial to avoid impractical reuse of resources, such as having a cinema and a restaurant share the same chairs, or worse, issuing that one facility operates only when the other is closed, which creates unnecessary dependencies and inefficiencies (circular dependency).
So, should we grow our space organically, allowing for multiple things going on continuously and concurrently, or should we take a stricter approach, by carefully composing the space every night and remake the whole city each morning?
It Depends … on the Roadmap
An approach works effectively only in relation to the specific goals and context of the project. The suitability of a particular architecture is determined by the situation and constraints. Therefore, one approach may be more practical or usable than another, depending on the circumstances.
A good architectural choice is one that is tailored to your roadmap. If your roadmap emphasizes:
- Focus: Ensuring that development efforts are concentrated on well-defined and prioritized areas.
- Feedback: Creating mechanisms for continuous feedback to refine and improve the product incrementally.
- Incremental Delivery: Delivering the product in small, manageable increments to ensure steady progress and adapt to changing requirements.
For such a roadmap, microservices could be highly beneficial. They offer not only a scalable architecture but also a good delivery mechanism that can minimize disruptions to production systems that need to be always operational.
Additionally, microservices can enhance:
- Scalability: Allowing different components to scale independently based on demand.
Choosing an architectural style like microservices must be a strategic decision aligned with your roadmap. Architecture must support your project goals effectively.
Addressing the main point: should we bail from microservices because of fear of circular dependencies?
Everything looks golden right? looks like microservices could be the ultimate agile tool, ready to bring to production every increment interdependently. Not so fast! there are of course other problems a part from the costs.
One of the pitfalls, is circular dependencies between microservices, of which there are two flavours
- Service-to-Service Dependency: In this scenario, a service is both the client and server of another service (or chain), but the two methods involved are different. This is a case of design failure, as the data needed from the second method should already be part of the context and be a parameter for the server service. Usually, this happens when we put only references in our messages rather than the whole method.
- Infinite Call Loop: This refers to a chain of calls in a distributed environment that forms an infinite loop. It’s easier to catch during end-to-end testing as it will never pass. However, it might pass integration tests, especially when multilevel, because the issue can become invisible due to mocking. The solution, of course, is a redesign.
Untangling spaghetti
Circular dependencies and spaghetti code often result from a lack of vision and proper design. To untangle them, follow these steps:
-
Redesign the Service: Circular dependencies often occur due to poor design and lack of cohesion. Split the service or method to ensure it is cohesive. If a service is handling multiple tasks, it’s a clear indication that it needs to be divided into smaller, more focused services.
-
Redesign Interaction: Proper application design can prevent the formation of spaghetti code from the start. Here’s how:
- Compose Information, Not Services: Instead of having services call each other in a chain, use batch processes to transfer information between services. This approach emphasizes true composition.
- Use workflows to manage complex processes, especially those that may fail and need retries.
- Implement retry-able sagas to handle failures gracefully.
- Follow Extract-Transform-Load (ETL): Adhere to ETL principles and strictly follow this order: collect, subscribe, process, and write. This minimizes the risk of data dependency.
- Enable Asynchronous Processes: Whenever possible, make calls asynchronously. If you’re dealing with both synchronous and asynchronous calls, consider them as transitions and states. If you’ve been relying solely on batch processes, enhance your approach with comprehensive workflows to manage “transition triggers.”
- Compose Information, Not Services: Instead of having services call each other in a chain, use batch processes to transfer information between services. This approach emphasizes true composition.
By redesigning both services and interactions, you can tame circular dependencies and streamline your architecture.
Links
- https://www.techtarget.com/searchapparchitecture/tip/The-vicious-cycle-of-circular-dependencies-in-microservices
- https://www.theserverside.com/answer/What-are-some-of-the-disadvantages-of-microservices
- https://en.wikipedia.org/wiki/Domain-driven_design
- https://en.wikipedia.org/wiki/Service-oriented_architecture
- https://en.wikipedia.org/wiki/Business_Process_Execution_Language
- https://en.wikipedia.org/wiki/Business_Process_Model_and_Notation
- https://www.factorio.com/
- https://en.wikipedia.org/wiki/Extract%2C_transform%2C_load
Footnotes
-
or worse your litter-box 😾 ↩