Today I think there are very few people in the software development world who have not heard of the term “microservices” … I see microservices mentioned almost on a daily basis, in discussions, articles, resumes etc… yet (unfortunately), not many people really understand or grasp the true concept behind microservices, let alone be able to design microservices based architecture.
This short article is my humble attempt to provide a quick, high-level overview of microservices and help demystify some of the misconceptions.
What is a microservice?
There are multiple definitions out there on what a microservice is, some talk about breaking down your application into multiple smaller functions, each with a single responsibility, while others talk about creating separate databases or 2 pizza teams, while they are right in their own perspectives and specific contexts, here is my simple, generic definition of a microservice.
Definition: A microservice is an autonomous service, modeled around a single business capability that can be deployed independently.
I know that’s a mouthful, but please note the underlined keywords in the definition above.
Autonomous: For a service to be called a microservice it should be autonomous, meaning, it should be able to stand on its own, function independently and be able to provide its (business) service even when other services are down / unavailable.
Single capability: A microservice should cater to a single capability, and not span across multiple business capabilities, otherwise it would become difficult to make changes to one without affecting others.
Business Capability: A microservice should cater to a business capability, else it would be no different than a helper function.
Deployed independently: You should be able to deploy your microservice independently, without requiring other services to make changes. Since each microservices is autonomous, aligned to a single business capability, this should not be a problem (if designed properly).
What is a microservices architecture?
Now that we understood what a microservice is, let’s try and understand what a “microservices architecture” is… to put it simply, a microservices architecture is a style of distributed architecture where the application is an aggregation of individual microservices, supported by infrastructure services.
As you can see from the diagram below, the square boxes (A, B, C, D, etc.) represent the individual microservices that work with each other to form an application. Also as you can see from the diagram, these microservices can be developed and maintained by separate teams with different skillsets, each team responsible for one or more microservices based on their team size, domain knowledge and technical expertise.
Microservices are generally supported by helper functions and infrastructure services that take care of cross cutting concerns (like authentication, caching, logging, application monitoring, api gateways, service discovery etc.). This helps microservices to remain lean and focus on the business logic and delegate non-functional aspects to other supporting services.
Characteristics of a well-designed microservice
Now that we understood a little bit about microservices and its architecture, let’s look at some of the characteristics of a well-designed microservice.
- Autonomous, single business capability and deployed independently – we already discussed this in the above sections.
- Owns and manages data (source of truth) for the business capability – since each microservice caters to a particular business function / capability, and has the most knowledge pertaining to that service, it makes a lot of sense for it (and the team behind it) to own the data and be the source of truth for that business function. Its ok for other services and materialized views to keep copies of the data for various purposes, but the microservice that owns the business capability should be the custodian and the authoritative source for the data in case of conflicts / mismatch. This also requires that each microservice maintains its own independent data store / table / blob storage (or an event store in case of event-sourced systems).
- Does not share / expose the data store (database) with other services – this is one of the best things you could do and thank yourself in the long run. A lot of you probably are a witness to this problem in your own workplace, where you don’t even know how many applications or services are directly connecting to your database… and even if you know, you can’t make any changes to your database because it might break those applications that you don’t have a control over. The best practice is to not allow other applications / services to directly connect to your data store. The next point talks about how to do this the right way.
- Exposes APIs and hides raw data and implementation details – your APIs should act as your contract and documentation to your consumers. Always define well thought out APIs and let other services talk to your microservices through those APIs so that you have better control over your data and implementation. Well thought out APIs rarely go through change, except when you need to enhance or add new functionalities, and when they do change, you should create a new version of the API and maintain the old version in parallel until all your consumers have migrated to the new version. This abstracts the implementation details from the API and allows you to freely make internal changes to your data, schema, business logic or business rules without affecting other services.
- Designed with failure in mind – resiliency is built into every aspect of a well-designed microservice so that it is prepared to handle failures, whether it’s the network, hardware or other application or infrastructure level failures. There are many well-established design patterns that can be implemented within the microservice to help achieve resiliency such as the circuit breaker pattern, bulkhead, compensating transactions, health check endpoint, leader election patterns, etc.
- Supported by infrastructure services – as mentioned earlier a well-designed microservices architecture will delegate cross cutting concerns like logging, AuthN/AuthZ, monitoring to other supporting services so that the microservices themselves are lean and focused on the business logic and does not have to do any of the heavy-lifting when it comes to cross cutting concerns.
- Smart endpoints and dumb pipes – This refers to the type of communication used by microservices to interact with each other. There are mainly two types of communication styles:
- Decentralized or choreography based approach (aka the “pull model” or “smart endpoints and dumb pipes”) where there is no centralized decision maker, no intelligent workflow service that makes routing decisions or decides the call sequence, there is only a simple messaging system (dumb pipes) used to deliver messages between smart endpoints (microservices). Each microservice informs the messaging system what events they are interested in receiving, and what events they will generate for others to consume, so that the messaging system can do the routing. This style of communication is better suited for scalability and extensibility as things are loosely coupled.
- Orchestration based approach (push model): In this style, there is a centralized workflow that contains the routing logic, decision logic and controls the call sequences. While this approach may be right in certain scenarios, this also creates tight-coupling between the orchestrating service and the microservices.
- Has an independent codebase, repository, and DevSecOps automation (CI/CD tooling) – each microservice should have a separate codebase, including all its libraries, tracked by version control system and a separate instance of build and release infrastructure (pipelines, static code analysis tools, unit testing, build & deploy scripts etc.). This will allow you to manage the source code (check-in/check-out, branch, merge, pull, commit, etc.) independent of other codebases and will allow you to release your service to production without depending on other codebases.
What is NOT a microservice?
Ok, now that we have a fair understanding of what a microservice is, let us look at it from a different perspective by understanding what is NOT a microservice.
- Simply having serverless functions with REST APIs is not microservice if it does not conform to our definition of a microservices.
- Simply using Kubernetes and running apps inside docker containers does not make it a microservices architecture, because technically you could also run monolithic apps inside a container.
- Horizontally slicing your apps into UI Service, business logic service and data service is not breaking it down into microservices, because they are not broken down by business capability.
- Simply using an event-driven architecture does not make it a microservices architecture, because you could do that between monolithic applications.
- Simply using a microservices framework like Spring Boot, Microdot, GoMicro, Moleculer (NodeJS) does not automatically convert your apps into microservices.
Advantages of using (or when to use) microservices
- Improved Productivity – it’s much faster for the dev team to build, test and deploy a small service than to build an entire application, it’s also easier to comprehend the functionality when the scope is small and well-defined. Having a small codebase also makes it easy to maintain and refactor the service continuously for better quality, stability, and performance. If your application is large and complex and your team is finding it difficult to manage and comprehend the application as a whole, and not able to move fast on new releases or experiencing quality / reliability issues, then it’s worth looking into microservices based architecture to see if it helps improve productivity by breaking down your monolithic application into smaller services based on business capability.
- Better resiliency – Microservices architecture provides better overall resiliency since it does not bring down the entire application when something goes down. Also, as mentioned earlier it is much easier to implement resiliency design patterns on a smaller scope than at an application level. It is also easy to troubleshoot issues, find the root cause, fix it, and redeploy the service.
If you see that your application is going down all the time for one reason or the other, then maybe it’s worth looking into the advantages of breaking down the applications into smaller, more manageable, autonomous services, so that failures are isolated and does not bring down the entire application.
- Polyglot programming – Microservices architecture allows you to choose the right programming language for the functionality being developed while still allowing interaction with other services written in different languages. This also allows you to leverage existing resources and skillsets. For example, if you have a team of data scientists proficient in python and R programming and wants to develop an artificial intelligence (AI) or statistical computing service, then you could allow that service to be developed in python / R while rest of the application is written in C#/Java/Go/Node.js. Microservices architecture not just allows you to mix and match programming languages, it also allows you to choose the right infrastructure services and databases (SQL/No-SQL/Graph/Timeseries) best suited for the specific capability being built.
- Constantly evolving application – Microservices architecture allows you to continuously evolve your services, whether adding new features or modernizing parts of your application. If your application is constantly evolving, causing frequent disruptions, then it is worth considering breaking your application into smaller chunks, grouped by business capabilities, so that you can test and deploy only the changed services, which is much faster and less error prone than testing and deploying the entire application.
- Better and selective scaling – Microservices architecture allows you to selectively scale only the required services. If you need to scale out certain business capabilities dynamically based on load, peak hours or seasons… then breaking down your application into microservices might be a good idea. For example, if you are into restaurant business and you know that your order intake module gets a huge amount of traffic between 11AM and 2PM, you can quickly scale out just the Order microservice and automatically scale it back once the peak is over. Scaling out the entire monolithic application just for the Order service might not be quick enough and/or might consume more resources.
- Ability to quickly react to market changes – Microservices architecture allows you to make frequent changes to your application with minimal impact. If you are in a highly competitive business and you need to quickly react to market changes / regulations, then your time to market would be much faster if your application was broken down into individual business capabilities so that you could make changes only to the right service, test and deploy it instead of having to test and deploy the entire application taking more time and effort.
- Better ownership of data – As mentioned earlier, each microservice encompasses a single (or parts of a) business capability and owns its data. This allows data to stand on its own and contain everything it needs to be meaningful with respect to the business capability. This also allows the data to be separated into smaller manageable chunks making it easier for the team to own, manage and maintain security and integrity of the data and designate it as the “source of truth”. This enables you to choose the right datastore for the type of data (SQL/No-SQL/Graph/Timeseries).
- Smaller team size – Since the scope of microservices are usually small and well defined, it allows you to have multiple small teams, organized around business capabilities. Smaller teams also tend to be well-connected, enabling better collaboration within the team and function independently.
- Reusable services – Since microservices are standalone autonomous services, they can be used by multiple applications if you design it well. For instance, in the polyglot programming example mentioned above, the AI / statistical service developed by the data scientists could be used by multiple applications / divisions if designed well (generic and loosely coupled).
Challenges (or when NOT to use microservices)
There are some challenges as well when deciding to move to a microservices based architecture:
- Has a steep learning curve – Distributed architectures are complex in general, you need to understand the art and science behind defining the scope and boundary of each microservice, how to discover and communicate between services, anticipate all kind of failures related to network, hardware, software etc., and how to persist data that spans multiple databases. So, if your dev team does not have the required skillsets or bandwidth to learn, or the company is not willing to spend money to train the team, hire experienced developers, or make the necessary process changes, then probably this is not the right time to embark on this journey.
- You might not get it right the first time – You might need to refactor the services over multiple iterations before you get it right, this takes time and experience, so if you are in a hurry with non-negotiable deadlines and do not have the time for trial and error, it might not be the right time to introduce microservices.
- Non-critical reads might be “eventually consistent” – In a microservices architecture, you might need to separate out critical and non-critical reads to be able to improve performance and scalability and might have to live with some inconsistent results for non-critical operations. If this is not something you are ok with, or you need time to understand this paradigm shift, or to convince the business stakeholders, then this might not be a good time to introduce microservices architecture.
- Hard to manage without automation – Generally in a microservices based architecture, there are a lot of moving parts… each having their own configurations for things like API endpoints, gateways, identity providers, databases, etc., very soon things can become hard to manage, and difficult to keep track of everything. The best way to solve this problem is through automation. You will need to automate the ‘build and deploy’ activities including things like static code analysis, unit testing, vulnerability scanning, release management etc. You will also need to set up Infrastructure as Code (IaC)) using tools like Terraform, Azure CLI, ARM Templates, CloudFormation etc. and manage configuration using Chef, Puppet, Ansible etc. So, if you do not have the required skillsets to set up CI/CD infrastructure nor intend to establish a DevSecOps practice and mature it, then getting into microservices might cause you more harm than good.