Software is now a key differentiator in every industry. Users expect a dynamic experience on a wide variety of platforms. Consequently, organizations must build applications that support frequent updates and scale to meet the demands of their customers.
In the past few years, we've witnessed an evolution in the software architecture space. Industry leaders like Netflix, Amazon, and eBay have moved away from monolithic applications in favor of a microservice architecture. Large unwieldy applications have been divided into smaller, loosely-coupled, and autonomous pieces.
In this article, we'll describe the differences between monolithic and microservice architectures and the circumstances which make microservices adoption suitable. We'll also address some common misconceptions about microservices.
A monolithic architecture is the traditional model for the design of a software application. The application is built as a single unit containing all the components required for functionality.
A monolithic Java application is typically packaged as a WAR or EAR archive. A retail web application may consist of several components including the front-end UI along with backend services handling user, search, catalog, inventory, and order functionality.
A monolithic architecture has a number of benefits:
- Simple to Develop
- We open the project in an IDE and see the entire application.
- Simple to Deploy
- We deploy a WAR file to a container like Tomcat or Jetty.
- Simple to Test & Debug
- Since all tasks run on a single process, it makes things much easier to test.
- Simple to Scale
- We scale by running N copies of the app on N servers behind a load balancer.
Here are some of the limitations:
- Tight coupling
- A failure or change in one component is likely to affect other components. If we need to change a component, the entire application must be recompiled and redeployed. Also, it is relatively easy to break an application even with a small change.
Large code bases are difficult to grasp
This presents a steep learning curve for new team members.
Prevents teams from working independently
Larger organizations often divide up their teams to focus on specific functional areas (Front-end UI, Registration, Payment, Messaging). With a monolith, the teams must coordinate their development efforts which hampers productivity.
Extended application startup times
A large application may take minutes instead of seconds to startup which slows down efforts such as testing and debugging.
Scaling can only be done on the X-axis of the scale cube
If we want to scale a component, we must run N copies of the entire application on N servers.
Married to the technology stack
It will be challenging to add a new component that relies on a non-native technology or switch to a different framework.
A microservice architecture consists of self-contained, independently deployable, and autonomous services. Each service is loosely coupled and bundles all its dependencies and execution environments. Services represent specific functionality such as user registration, order payment, and messaging.
A Java-based microservice could be a standalone executable JAR that has embedded Tomcat and exposes an API. Services may use synchronous communication with HTTP/REST or asynchronous message-based communication with something like AMQP. The ubiquitious convention for data format is JSON.
What problems do microservices solve?
Microservices solve the following problems:
- We can now scale each microservice as necessary across multiple containers or through a larger container
- Slow deployment velocity
- Teams can now develop and deploy their services independently of other teams
- Design autonomy
- We have the freedom to employ different technologies, frameworks, and patterns for each microservice without touching anything else.
If you don't have these problems, your best approach is to avoid microservices until you find yourself faced with these problems.
What problems do microservices introduce?
Microservices offer superior agility and scalability but these come with a complexity premium.
A stack trace in a monolithic Java application will lead you straight to the problem. Tracing a problem back to its source with microservices requires assigning a Request ID to each request and logging it somewhere.
Communication between Services
In a monolithic application, components invoke one another via language‑level method calls. There's no outside communication required for the Order component to talk to the Inventory component because they live in the same memory space.
In a well-designed microservices architecture, services are unaware of other services. This means that there shouldn't be any direct communication between them. This adds some complexity to the communications model.
We would need to implement an abstraction like a message queue. The Order service may publish an orderCreated event to the queue. The Inventory service can subscribe to this queue if it needs to know about these events in order to do its job.
Each microservice should have its own private data store and should not be shared with others. This may result in duplicate data and consistency challenges if the Payment service needs information from the Accounts service to perform its job.
Misconception 1 - Microservices are a term for any app that runs in the cloud
- Running an application in the cloud does not make it a microservice. Cloud infrastructure do make the deployment of microservices more feasible because of rapid server provisioning and automation.
- Misconception 2 - Microservices are a term for any app that runs in a container
- Running an application in a container does not make it a microservice. Container technologies like Docker are certainly a common way to packaging and deploying microservices though.
- Misconception 3 - Microservices improve code quality
- Any application architecture is susceptible to poor code quality. The principles of clean code should be followed no matter which approach you take.
Refactoring a Monolith into Microservices
A popular strategy for migrating to a microservice architecture is to gradually break pieces off the monolith and turn them into microservices. This approach limits risk and can be done incrementally until the monolith no longer exists.
This results in a two-pronged plan:
- Identify existing monolith components that can be decoupled
- Mandate that new functionality should be developed as a microservice
A key challenge is to design and develop the integration between the existing system and the new microservice. One solution is to introduce glue code that allow them to talk to each other.
An API gateway can also help combine many individual service calls into one coarse-grained service, and in so doing reduce the cost of integrating with the monolithic system.
Microservices are sexy. Everyone wants to say they are working on an app using this architecture. But the temptation to abandon a monolithic architecture for a microservice should be tempered by measuring how much value can be gained.
It's a widely accepted best practice to build new applications with a monolithic approach, and only consider moving to microservices when there is justification backed by metrics such as performance monitoring. It's important to note that many of the disadvantages outlined above for monoliths surface only when the application has reached a certain size.
For large companies with over 100 engineers, microservices are an avenue to continuous deployments, independent team-based development, and agility to move to new technologies. For everyone else, adoption of microservices does not make sense and can negatively impact the success of the software project.
To summarize, it's smarter to start with a monolith until scalability and development velocity demand evolution to microservices.