Architecture patterns can influence the success of a project as well as the ability to deliver new features in the future and the degree of flexibility of the business. They can help the IT organization to set up the project for future growth by anticipating the needs of the business.
The architecture choice helps us optimize the work for different targets: speed of delivery, budget, flexibility, etc. The different patterns are useful in different situations.
In the monolithic pattern, the application is a single unit, and all functionalities get developed and shipped together—all-or-nothing. It is appropriate for smaller independent applications with a small number of users, for example a simple website. The monolithic pattern used to be the predominant pattern for all applications, but as businesses developed bigger and more complex systems, the monolithic approach became problematic. The codebase became very big and the task of keeping it organized, well-structured, and even understood by the developers became impossible. As time passes, developers often forget or lose the knowledge of certain parts of the system or the internal dependencies, which makes adding new features a slow and painful process, riddled with bugs. The complexity of the codebase limits the team and code scalability and increases the cost of adding new features.
Service-Oriented Architecture (SOA) emerged in the early 2000s as services started being separated by function with the goal of reusability. Services were coarse-grained and usually communicated with each other via a central Enterprise Service Bus. SOA addresses some of the monolithic system concerns by separating the codebase into smaller pieces, however it introduces team dependencies as it strives to optimize for reusability.
Microservices is the next step in the evolution of architecture patterns. It originates from the SOA approach, but the services are more fine-grained and reusability is no longer the primary focus. The software teams are ablе to function more independently, and sometimes common functionalities can be duplicated to preserve team autonomy. Microservices strive to optimize for scale. For example, an e-commerce system with product catalog, ordering, payments, and warehouse functionalities can have multiple teams working independently on each part of the system. In the past decade, microservices have become a dominant architecture pattern for large projects.
Monolith vs. Microservices
Monolithic applications have a single database, which often becomes a point of contention limiting the ability to scale to more users. In addition, the standard three-layer architecture traditionally uses a relational database. Generally, relational databases have a limit for vertical scalability and the cost increases dramatically as more users need to be supported.
As application usage and feature requirements grow, more teams need to work on the code and they have different/conflicting technical requirements. In this case, the architecture can evolve into microservices, so each team can work on their features independently and optimize their technical decisions for their business problem. The independent components can be developed separately and then combined like Legos to achieve more complex features.
In microservices, each service has its database(s) that can be optimized for the microservice’s responsibilities. We can choose the right database technology—relational or non-relational—and manage the scalability of each microservice independently.
In addition, each microservice can have its UI, which is developed by the same team, giving the ability to deliver end-to-end features by a single team and reducing the need for cross-team coordination. Each team should be able to independently deliver value to its customers whether internal or external.
This makes the microservice approach more appropriate for complex systems with many requirements and features.
What Are Microservices?
Microservices decompose a complex application into multiple pieces that can be developed independently. Each piece is small, aligned with business functionality, and independently developed, deployed, and released. It owns its data and exposes interfaces for communication following a contract. It needs to be loosely coupled with other microservices in order to support team autonomy. Since it’s separate, the team has the freedom to choose the technology language, platform, and database.
“Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure.”
Melvin E. Conway, 1967
Conway made his observations in 1967—even then software development was running into limitations when scaling—and at that point, the approach was to modularize the systems. The limitations we run into are based on human behavior: people are more likely to communicate within their group instead of with outside groups, hence the group boundaries become software architecture boundaries. Studies have investigated this link and confirmed the observation. They found that co-located, focused product teams often created more tightly coupled monolithic software, while open-source projects were more modular and decomposed.
Microservices architecture needs to be implemented with Conway’s law in mind. If your organization’s team boundaries are not well aligned with the boundaries of the services, then the microservices approach will struggle to produce all the benefits. If the boundaries do not align, different teams will need to make changes to the same microservices, which can result in confusion, tension, and lack of autonomy. Such tensions reduce productivity, can affect our deadlines, can cause our architecture to drift in the wrong direction, reduce quality, etc. When we have an architecture that spans multiple teams, the microservice pattern can help us define the boundaries of each team so that each team can work independently from the rest, and the overall resulting architecture is aligned with the organization’s structure and goals.
When systems and teams grow, one of the natural approaches to try is putting everyone on the same team in the hopes of improving communication and coordination. However, big teams of more than 10–12 people naturally split into smaller subgroups as one person can’t communicate with so many people effectively. Communication within a big team is more complicated and takes more time from each individual, thus developer productivity decreases. Companies like Amazon have adopted the 2-pizza teams model: the team should be sized so that they can be fed by two pizzas (8–10). Each team has responsibility for a small piece in the overall architecture and it can function autonomously. Each team owns the lifecycle of its services, manages the relationship with its customers (internal or external), and evolves it based on the customer’s needs. This structure, combined with microservice architecture, allows more freedom to experiment and adapt to changing business and consumer needs, which makes the company more innovative.
Microservices allow organizations to scale by splitting the work into independent pieces that can be developed autonomously. Multiple teams can work in parallel without affecting each other, as long as they abide by the agreed-upon contracts. Because the teams are autonomous, the developers can focus on their small piece of the solution, understand the business domain, feel more empowered, and increase their efficiency.
Microservices allow decentralization of the technology decisions, including the technology stack and database, so that each piece can be implemented with the most appropriate choice. This increases efficiency and allows higher scalability and resiliency of the services. Decentralization also allows the teams to perform faster iteration cycles and reduce the time to market new features. This reduces the time needed for the OODA (Observe, Orient, Decide, Act) loop to be completed and allows the business to be more agile and base its decisions on customer data rather than guesses. By releasing smaller features more often, we reduce the risk and the result is a more stable system. If one small service fails, the rest of the system continues to function, whereas in the monolithic approach the whole system goes down.
Microservices support higher scalability and resilience as well as faster iteration cycles, as each team can deploy and release their features independently. Since smaller changes are getting pushed to production, each deployment and release carries less risk than a big monolithic release.
Microservice architecture has a lot of benefits, but it also poses challenges to the organization and development as it is more complex than the monolithic approach. The increased complexity can be tackled by carefully decomposing the solution so the team can be truly independent. Each piece should be able to work on its own, and each team needs to have its own roadmap and requirements. Product architecture practices should help to decompose the product into separate modules.
In addition, automating the deployment and testing process becomes a must and increases the cost of the solution.
Microservices require specific team skills to be present in each team in order to be truly autonomous. The team should be able to deliver business value to its customers and own the development end-to-end. The team should have knowledge of development, testing, deployments, etc. Often, developing a common toolset can be useful so teams don’t have to be specialists in everything and to make sure consistent patterns are used throughout the organization.
When implementing a microservice architecture, the number of microservices can grow to hundreds or even thousands. Managing so many services requires a lot of additional work. In addition, if not careful, the failure of one service can cause a cascading failure across the organization. In order to address this, the teams should use service orchestration and discovery tools, manage the service versions, including backward compatibility, and ensure all services are fault-tolerant. The distributed nature of microservices requires the use of specialized monitoring and tracing tools. In addition, the surface area required to be secured increases, and the solution requires inter-service communication security as well as automated security checks to ensure all teams are following security best practices. Usually, teams develop common patterns that can be reused for these cross-cutting concerns.
Another challenge in microservices is that each service has its own piece of the data, and in order to produce a holistic view of the data, it needs to be processed and consolidated, which requires extra effort.
When Should We Use Microservices?
Given all the challenges when using microservices, should you consider using them in your project? The answer is: it depends.
If your project is a large system that requires agility, higher delivery speed, or scalability to thousands or millions of users, then microservices are your best choice. If you need to tackle complex integrations with multiple technologies, third-party, or B2B systems, microservices can also help you.
If you’re struggling with multiple teams working in a single, large, tightly coupled codebase, gradually rewriting your system into microservices can alleviate your pains. If your teams are having a hard time adding new features, and whenever a change is made something unrelated breaks, this is a sure sign your system is tightly coupled and could benefit from modularization. If your teams need to coordinate constantly to deliver business value, then you should rethink the team/service boundaries and make sure the separation is appropriate.
On the other hand, if you want to use microservices just because everybody else is doing it, you should think carefully about it. Microservices have additional overhead and should not be used for simple applications that don’t require high scalability. Microservices require an initial investment for setup, which might not be needed if you just need a quick MVP. The investment will pay off in the long run, however it will not provide a short-term ROI. Finally, in order to make sure your microservices project is successful, you need to have experienced people on the team. Monolithic applications still have a place in modern software development and should not be discarded right off the bat.
Overall, microservices allow businesses to scale, grow, and be more agile and innovative, but you need to make sure you have a plan for managing the additional complexity and risk associated with them. The decision of whether to use them can seriously affect the development duration and cost, so the trade-offs need to be considered carefully. As the business grows and increases its customer base, the need for microservices can arise, so the architecture should be reevaluated and allowed to evolve.