As new Internet-based companies get started, they will often organize all back-end code into a single codebase. This code is deployed onto one tier of servers, representing their primary back-end infrastructure. Using a single application back-end is often referred to as a monolithic approach. This approach is contrasted with a service-oriented architecture, where application logic is divided into a suite of services (more recently referred to as microservices). Each service is independent from the others, with a separate codebase and deployment target.
Maintaining a monolithic back-end can offer some advantages to a small team, but will create challenges as the application grows. In this post, we will examine some of the considerations for maintaining a monolithic back-end, versus segmenting out logic into discrete services.
Duplication of Logic
A single application often contains duplicate logic in many places. As changes are planned to the application, those changes will need to be applied to all places where the duplicate logic exists. By breaking the application up into services, this duplicate logic can be consolidated into a single service. Changes to that logic will then need to be made in only one place. In a monolithic application, this code duplication can be reduced through an object-oriented approach, defining independent classes for each major type of functionality. However, the functionality provided by these shared classes will still need to be reviewed and understood before the class can be consumed. This creates overhead for the team, requiring each developer to maintain an understanding of a vast set of classes that can be incorporated into the main application.
Clear Contracts between Services
Migrating to a service-oriented approach requires the definition of clear contracts between services. With a well-defined interface, service functionality becomes intuitive. In order to allow other teams to interact with the service, the owning team will generally publish documentation specifying the interface. This will be structured like an API, with function calls, inputs and outputs delineated. This interface documentation will be very useful for new team members, allowing them to learn about the service independently. Consuming teams can also ignore the inner workings of the service, as long as they maintain clear interactions through the service’s interface. This will allow development for consuming teams to proceed more quickly.
Speed of Product Evolution
Organizing around services allows small, nimble teams to be assigned to each service. These small teams usually consist of a few developers, a product manager, a devops engineer and a tester. A team this small can enhance their service very quickly. Vetting proposed changes to an application with all affected parties is usually the largest encumberence to making rapid decisions. Decision making is slowed down as the organization involved becomes larger. Having a small, autonomous team assigned to each service will allow this decision making to proceed quickly, as all of the individuals necessary to review and approve a decision is minimal. On a large monolithic application, the engineering team will usually be segmented into smaller sub-teams, like search, content management, community, etc. This does offer some separation of concerns. However, if all of these sub-teams are contributing to a common code base, then dependencies will still emerge and the decision making overhead will continue.
Part of establishing a new service is to provide the boilerplate functionality to perform many common functions. These can include call marshaling, security, response formation, database access, etc. Once a single service is established, much of the service interface code can be templatized and re-used in other services. This will lower the initial roll-out cost for services over time. It is helpful to organize this shared code into a set of libraries or independent code modules by function. The shared code should be stored in a separate project within your code repository. You can also consider whether to assign a particular developer or team ownership over the shared code. Even if ownership is initially distributed, like an open source project, you should still assign a single individual to conduct code reviews of new contributions.
Testing is much easier on a set of individual services than within a large application. With a monolithic application, there will generally be more logic intertwined and linked dependencies between functions. The set of regression tests necessary to ensure that a change in the application hasn’t affected other parts of the functionality will be large. Your QA team will need to run through these regression tests before every major release. As the monolithic application grows in size, this regression testing overhead will increase. By breaking out the application into discrete services, code changes can be more isolated. A code change in a single service will then require only regression tests within that service’s codebase to be run.
Risks with Service Oriented Approach
One disadvantage of using a heavily service oriented approach is the processing overhead associated with making the remote calls to services from the application that provides the initial entry point into your back-end. A call to a service on a separate server tier will always take longer than running that code on the same server. Also, if a single service is down, that can impact performance of the entire back-end application, as incoming requests continually wait for a response from the slow service.
These risks can be mitigated. First, the performance of each service should be measured and optimized as much as possible. Response times should be tracked over time, and flagged for refactoring when they cross a threshold. To mitigate the risk of cascading slowdowns stemming from a single service failure, the application receiving client requests should be designed with service failure logic. This would include a time-out on a service call and utilization of a default value in constructing the overall response to the client.
With a small team, it is easier to maintain a single monolithic application. As we discussed, the main cost of maintaining a single application is the coordination overhead required when making design changes. With just a few engineers, this coordination overhead is minimal. As the team expands, this communication overhead will require more time. I have seen large teams working on a monolithic application even have to schedule meetings between sub-teams to vet proposed design changes.
I think a team size under 5 engineers can easily maintain a monolithic application. As the team expands beyond this, it’s better to start dividing the monolithic application into separate code bases, deployed as services. This allows different developers (and eventually teams) to be assigned to each service, and their work to be isolated from the others.