
A lot has been said about the benefits of microservices, but ultimately, we have seen some trends considering the return of monoliths. Companies and engineers blame microservice architecture for performance, latency, and productivity issues. However, the lack of maturity and experience in the industry has led to overlooking important design decisions and making it harder to deliver software. Microservices is not the ultimate goal but a journey I want to tell you based on my experience.
A real motivation
Twenty-five years ago, the first company I worked for was a product company, and its two main products were an ERP and an accounting system. A single executable was enough for a small number of users. Later, the increased number of users forced us to adopt a different architecture. We wrote a persistence layer and moved some logic into stored procedures. The client-server architecture allowed us to create a reliable data persistence layer, improve performance and scalability, and support business growth. Multiple users could access the system concurrently without concerns about locks or data corruption, ensuring performance.
The internet boom made us realise that a business layer contenting all business logic would be a better approach for supporting multiple clients – desktop and web. We found that adopting an N-layered architecture was a wise choice on that occasion. We migrated all business rules into a business layer and distributed that via COM+.
Our first work with distributed components gave us an idea of the upcoming complexities. Coordination, latency, debugging and troubleshooting were only the beginning. However, it was still a monolith distributed via COM+.
Decisions must be recorded
We went from serving a few clients to hundreds, transitioning from desktop to web clients and building integrations to third-party services. As you should have realised, our motivation to use different technologies was driven by business growth and a shift in the industry pushed by the Internet.
The adoption of Microservices opened up the “pandora’s box” of technology choices that your company can adopt. There was a time when relational databases used to be sufficient. Sharding and table partitioning for large databases and tables are still valid patterns. However, microservices went beyond, allowing every service to pick its persistence layer, language and framework. Nothing is wrong with this as long as your design principles align with your business requirements and not your desire to use new tech.
Architecture Decision Records (ADRs) are a great way to document decisions in software projects. They provide a structured approach to record architectural choices, including the context, the options considered, the final decision, and the rationale behind it.
Maintaining an ADR is paramount from the onset of the workload and throughout its lifecycle. It allows teams to keep a fresh understanding of the design decisions and helps new team members learn about the project’s history. ADRs are vital for ensuring design decisions align with functional and non-functional requirements.
Boundaries are hard to identify
The software company grew, increased its customer base, and hired more developers. Yet, it was difficult to understand the whole system, impossible to train people quickly, and the learning curve was high. Engineers used to take longer to start delivering features. Aligning tasks according to the team’s domain knowledge minimised all these problems. Despite that, breaking down that monolith into modules wasn’t straightforward.
It is hard to work on a system with no boundaries because, at some point, the functionalities will overlap other functionalities, and they will start competing against each other to access the same resources. Exploring a business domain focusing on data structures will hide differences and highlight misleading similarities, ultimately nudging your design into unnecessary coupling.
Domain-Driven Design incorporates effective techniques for identifying bounded contexts:
- Ubiquitous Language: Developing a shared language between developers and domain experts helps clarify communication and ensures that terms are understood consistently across all contexts.
- Context Mapping: Context maps allow us to visualise and define the boundaries between different contexts, enhancing our understanding of system interactions and boundary delineation.
- Event Storming: Conducting workshops to collaboratively map out events, commands, and processes within the domain helps identify natural boundaries and contexts.
- Subdomain Analysis: Analysing the domain to identify subdomains can help map these to bounded contexts, providing insight into different areas of responsibility within the domain. Analyse
- Team Organisation: Aligning bounded contexts with team structures ensures that each team is responsible for a single bounded context, fostering a clear and cohesive model.
- Codebase Structure: It’s essential to organise the codebase to reflect the bounded contexts, with each context having its own module, code repository, models, and services for better maintainability and clarity.
These techniques helped us to break down that monolith into modules, giving life to a modular monolith.
Defining which features belong to each service is not an easy task. You may only know if your boundaries are right once it is late in your software development process. The problem scales up when you have multiple developer teams building components that, at some point, will interact with each other.
Modular monolith first
Modular monolith is a software architecture where a monolithic codebase is organised into separate, interchangeable modules or components within a single runtime environment. To solve friction between the teams related to frequent changes in the source code and different priorities, we adopted the sharing code ownership responsibility model and ensured code quality through continuous integration. Now, any team member could raise changes in another’s team modules, which helped us to reduce the dependency between the teams, increasing autonomy.
If people can’t build monoliths properly, microservices won’t help.
Modular monoliths and microservices have distinct characteristics, and there are several problems that modular monoliths do not encounter when compared to microservices. Here are some key differences:
- Granularity: Modular monoliths have a smaller surface area of code to maintain, whereas microservices involve a higher degree of granularity and decomposition that can complicate monitoring, management and troubleshooting.
- Complexity in Communication: Modular monoliths operate within a single codebase, simplifying communication between components. In contrast, microservices often face challenges related to direct client-to-service communication, such as potential mismatches between API definitions exposed by each microservice.
- Transactional Consistency: In a modular monolith, atomic transactions across components are easier to manage since they operate within a single context. Microservices, however, typically require embracing eventual consistency, making it more complex to handle transactions that span multiple services.
- Resource Management: Modular monoliths generally have lower initial resource needs in a singular environment. On the other hand, migrating to a microservices architecture often requires significantly more resources due to the distributed nature of services.
- Development Effort: Development within a modular monolith can be less complex and more straightforward than microservices, where managing multiple independent services requires additional development efforts to handle orchestration and communication.
Microservices decision
The idea of using microservices came much later when we understood how users, applications, businesses, and teams behave. Gathering feedback from the teams was crucial for microservice adoption. They guided our decision-making process, which enabled a breaking down strategy:
- By combining telemetry insights with business priorities and module characteristics, we could make informed decisions on which modules to transition into microservices, ultimately improving the overall architecture and performance of your application. We realised that part of our application was more demanding than others, analysing the telemetry data to monitor performance and identify bottlenecks, high latency operations or frequent errors destabilising our application.
- By assessing dependencies between modules, we could conclude that modules that were called together or have a high level of interactions may stay together. On the other hand, modules that cause performance issues must be separated. Conversely, modules that can function independently may be prime candidates for microservices.
- By considering the business value of each module and prioritising those that significantly contribute to core business functions or customer satisfaction for decomposition, we ensure that the most critical parts of the system are optimised and can evolve independently.
Combining all those answers helped us elaborate a plan for breaking down our modular monolith into microservices.
Takeaways
As you can see, Distributed Application Development is not exclusive to modern architectures such as microservices. Twenty years ago, I went through this journey to solve scalability problems using available tools, but we face the same challenges today. We must think carefully before moving to microservices.
The industry is still struggling to understand whether the term “micro” is overemphasised to the point that many companies have deployed hundreds or thousands of services or if what is a “service” is misunderstood.
I must also emphasise that high cohesion and low coupling without bounded context lead to the fragmentation of services, which is a much bigger problem. It kills software’s performance, introducing unnecessary latency while increasing complexity. It becomes difficult to the extent that developers don’t understand how all those microservices work together—they lose integrity.
Microservices are not bad at all. They are the best way of building large and complex systems, which requires a level of sophistication that is only sometimes evidenced. However, all the advantages of microservices are also disadvantages, which means you need to consider the microservice trade-off before making an effective choice.

Leave a comment