Over the last few weeks we’ve looked at cloud computing and some of the technologies that support it, like virtual machines, hypervisors, containers, Docker and Kubernetes.
This week we’ll explain the concept of microservices, and why they’re a great fit for containers and cloud-based computing.
Let’s say we have to develop an online shopping website. We’ll probably first think in terms of a layered approach to the development. There will be a number of different layers communicating with each other. There will obviously be a user interface layer which will be a web-based front-end. There will be an application logic layer dealing with products, inventory, shopping carts, orders, checkout, client info, billing, invoices, etc. There will be a database layer to save all the business data generated in the application logic layer.
How would we design this system?
Monolithic Architecture
Let’s first use the traditional monolithic architecture to develop the online shopping application.
With this approach, we’d build the application as a single, large unit. All of the components (user interface, logic, database, etc.) are developed as part of this single unit. The components are inter-connected and inter-dependent. The entire application will be tested as a unit. Each small update will force an entire re-test and redeployed of the whole system.
The application has to be scaled as a unit, with copies of the application being deployed to extra servers. This leads to higher infrastructure costs and less flexibility. The development team usually uses a single language and a single framework with one large codebase in a single repository.
We can also imagine that as the business changes, and additional requirements are put on the table, the whole development cycle will take longer and longer. Testing, debugging, deployment and scaling will get harder. The application will become more brittle and less robust. Maintenance costs will rise. Soon we’ll have to start thinking about rewriting the application as the system gets more unstable.
The problems with this monolithic approach include:
- The application becomes very large and complex.
- Components are too tightly coupled together.
- The update release process takes longer.
- For every change, the entire application must be rebuilt, retested and redeployed.
- The entire application has to be scaled as a unit, instead of simply scaling the required components.
- A bug in any component can potentially crash the entire application.
- The development teams must ensure that any work they do doesn’t affect any other team’s work.
Microservices Architecture
A possible solution to all the previous problems is to use a microservices architecture.
Microservices is one of the most popular modern architectural approaches to developing software systems.
The main idea behind this architecture is to split the monolithic application into smaller, independently deployable, loosely coupled, modular services. Each service runs a single process, which in turn can then run in its own container. This give us all the benefits of container-based development (as covered in earlier posts).
The various services communicate with each other through a well-defined, lightweight protocol. Adding additional services is vastly easier than with a monolithic architecture. The microservices architecture is ideal when we have to support a wide range of platforms and devices.
Microservices can make our application more robust and less brittle. Running each service in its own container makes it easier to scale the application on demand. Extra services can just be spun up to handle fluctuating demand.
There are some important design questions to answer before we can start developing a microservices based system:
- How do we decompose the application?
- How many services do we need?
- How big or small should these services be?
- How do they communicate with each other?
- How do services find each other?
- What code goes where?
- How do we control and manage the codebase?
Microservices are much more complicated to do properly than most organisations think. A big problems with microservices is that many enterprises trying to build a microservices system end up building a quasi-modular monolithic architecture instead.
Microservices Best Practices
There are no easy, follow-the-numbers rules when answering the previous design questions. However, if we follow the current best practices, we’ll probably be on the road to a successful development. These practices include:
-
Autonomous. The services must be self-contained, autonomous and independent. They must be developed, tested, deployed and scaled independently of other services. We must be able to change implementations without coordinating with others. This is the real value of microservices.
-
Separation of concerns. We should prefer services aligned with “seams” in the problem domain. Each service must be focused on one task, and do it well. Split the components based on functionalities, e.g. products, shopping cart, checkout, inventory, clients, etc.
-
Cohesive. The services must be small and make up a cohesive unit. They should represent a compartmentalised problem (aligned with a bounded context). Can we rewrite it in a week or two if necessary?
-
Independently deployable. If we always test our service with other services before it gets released, it is not independently deployable.
-
Loosely-coupled. The interface to a microservice is a public API. This interface should only be changed with great care. When consuming an API, we must use the minimum data that we can to reduce coupling between services. We should make internal and external data representations different.
Communication between Services
There are three main ways that microservices communicate with each other:
- Using a REST API. Each service has its own endpoint and API specification. This uses the HTTP request/response pattern. This gives us synchronous communication only. Most literature tends to describe this mechanism.
- Using a message broker. This can use either a publish/subscribe model (pub/sub; store and forward) or a point-to-point (P2P) model. This allows asynchronous communication.
- Using a service mesh. This is popular when using Kubernetes.
Pros and Cons of Microservices
There are a lot of advantages to a microservices architecture:
- The application is more modular. This makes it easier to understand, develop, test and maintain.
- Very easy to scale. Since microservices are implemented and deployed independently of each other, they can be monitored and scaled independently.
- Easy to integrate to legacy systems. Microservices can be used to modernise existing monolithic systems.
- Distributed development with smaller autonomous teams developing, deploying and scaling their services independently.
- We can adopt different technology stacks per team based on specific business requirements.
- Facilitates CI/CD in our development process.
Microservices also bring a lot of new challenges:
- Correct design and service demarcation is a problem. Service-based design != microservices.
- The application is now a distributed system with all the attendant difficulties and additional complexity.
- More communication configuration. What happens when one service expects a response from a service that is not running or is blocked?
- Services need to discover each other. A service discovery tool/system is needed.
- More difficult to monitor with multiple instances of each service distributed across multiple servers.
- We will definitely need a CI/CD pipeline for deployment.
- Load balancing between services is required.
- Managing data consistency and transactions becomes more difficult. Services should be responsible for their own data.
- Higher infrastructural costs.
- Higher HR hiring costs if different teams use different technology stacks.
Monolithic and Microservices Comparison
Some of the differences between monolithic and microservice architectures include:
Monolithic architecture | Microservices architecture |
---|---|
Single application on server-side system | Every application function is its own service |
Easy to develop, deploy and manage | More difficult to deploy and manage |
Single, large executable artifact | Each service runs in its own container |
Inter-process communications | Services communicate via APIs |
Highly inter-dependent components | Independent components |
Dependent on a single language and framework | Can be language and framework independent |
Single technology stack chosen by architect | Each team can choose its own technology stack |
Difficult growth | Easy growth; less risk in change |
Difficult to scale | Easy to scale under demand; independent scaling |
“Hero” deployment | Can iterate easily through a DevOps pipeline |
There is no one-size-fits-all in software design. We need to use the correct architecture for the problem. Not all applications need to be developed as microservices. Microservices offer benefits in the right situation, but add lots of complexity.
What’s Next?
You should now have a better understanding of microservices.
If you’d like to do more reading, Wikipedia has the usual good and (relatively) easy to read article on microservices.
The https://microservices.io/ website has a lot of really good content, including microservices patterns.
The Continuous Delivery channel on YouTube has some excellent videos, as does TechWorld with Nana.
Don’t forget to share your comments and experiences.
Stay safe, and I’ll see you next week!