Introduction to Microservices
In this chapter, you will be introduced to microservices and the motivation behind them. You will understand the key benefits and common issues of the microservice architecture model and learn when to use it, as well as getting some microservice development best practices. This knowledge will help you establish a solid foundation for reading the next chapters and give you some ideas on what challenges you may face with microservices in the future.
In this chapter, we will cover the following topics:
- What is a microservice?
- Motivation to use microservices
- Pros and cons of microservices
- When to use microservice architecture
- Role of Go in microservice development
What is a microservice?
Companies worldwide have used the microservice architecture model so widely that it has almost become a default way of software development. Those companies have tens, hundreds, and even thousands of microservices at their disposal.
So, what exactly is the microservice model?
The microservice architecture model is organizing an application as a collection of services, called microservices, each of which is further responsible for a certain part of application logic, usually defined by a particular business capability.
As an example, consider an online marketplace application. The application may have multiple features, including search, shopping cart, payments, order history, and many more. Each feature can be so different that the code may (and, in certain cases, should) be completely independent of the rest of the application. In this example, search and payments technically have nothing in common. In the microservice architecture model, each component would be an independent service playing its own role in the system.
Organizing each part of the application as a separate service is not necessarily a requirement. As with any architecture model or any aspect of software development, engineers need to be careful with choosing a particular approach or solution – doing an initial analysis and understanding the solution under the given conditions.
Before we proceed to the key benefits and downsides of microservices, let's see what challenges you could face when the application is not separated into multiple services.
Motivation to use microservices
In order to understand the motivation behind using the microservice architecture, it is very important to see the opposite approach – when the application is built and executed as a single program. Such applications are called monolithic applications or monoliths.
Monolithic architecture is, in most ways, the simplest model to implement since it does not involve splitting the application into multiple parts that need to coordinate with each other. This can provide you with major advantages in many cases, such as the following:
- Small code base: Splitting an application into multiple independent parts may significantly increase the size of the code base by introducing extra logic required for communication between the components.
- Application logic is still loosely defined: It is very common that parts of the application or the entire system go through major structural or logical changes, especially at the very early stages of development. This might be caused by a sudden change of requirements, priorities, changes in the business model, or a different approach to development. During the early stages of development, iterating fast can be critical not only to the development process, but also to the entire company.
- Narrow scope of the application: Not every service requires a decomposition and division into separate parts. Consider a service for generating random passwords – it has a single logical feature and, in most cases, it would be unnecessary to split it into multiple parts.
In all of the preceding cases, monolithic architecture would be a better fit for the application. However, at some point, services get too big to remain monolithic. Developers start experiencing the following issues:
- Large application size and slow deployments: At a certain point, an application can become so big that it can take minutes or even hours to build, start, or deploy.
- Inability to deploy a particular part of the application independently: Not being able to replace a part of a large application can easily become a bottleneck, slowing down the development and release process.
- Higher blast radius: If there is a bug in a certain function or library widely used across the application code, it is going to affect all parts of the system at once, potentially causing major issues.
- Vertical scalability bottleneck: The more logic the application has, the more resources it needs in order to run. At a certain point, it can get hard or impossible to scale the application up even further, given the possible limits on CPU and RAM.
- Interference: Certain parts of the application can heavily load CPU, I/O, or RAM, causing delays for the rest of the system.
- Unwanted dependencies between components: Having the entire application represented as a single executable leaves room for unnecessary dependencies between the components. Imagine a developer refactoring a code base, and making a change suddenly affects some important parts of the system, such as payments. Having more isolation between the components gives more protection against such issues.
- Security: A possible security issue in the application may result in unauthorized access to all components at once.
In addition to the possible issues we just described, different components may have different requirements, such as the following:
- Resources and hardware requirements: Certain components are more CPU-intensive or memory-intensive and may perform I/O operations at a higher rate. Separating such components may reduce the load on the entire system, increasing system availability and reducing latency.
- Deployment cadence: Some parts of the system mostly remain unchanged while others require multiple deployments per day.
- Deployment monitoring and automated testing: Certain components may require stricter checks and monitoring and can be subject to slower deployments due to multi-step rollouts.
- Technologies or programming languages: It is not uncommon that different parts of the system can be written in different programming languages or use fundamentally different technologies, libraries, and frameworks.
- Independent APIs: Components may provide fully independent APIs.
- Code review process: Some components may be subject to a stricter code review process and additional requirements.
- Security: Components may have different security requirements and may require additional isolation from the rest of the application for security reasons.
- Compliance: Some parts of the system may be subject to stricter compliance requirements. For example, handling personally identifiable information (PII) for users from a certain region can put stricter requirements on the entire system. Logical separation of such components helps to reduce the scope of work required to keep the system compliant.
With all the preceding issues described, we can see that at a certain point monolithic applications can become too big for a one-size-fits-all model. As the application grows, certain parts of it may start becoming independent and have different requirements, benefiting from a logical separation from the rest of the application.
In the next section, we are going to see how splitting the application into microservices can solve the aforementioned problems and which aspects of it you should be careful with.
Pros and cons of microservices
In order to understand how to get the best results from using microservices and which issues to be aware of, let's review the pros and cons of the microservice model.
Benefits of microservices
As previously described, different application components may have fundamentally different requirements and at certain points diverge so much that it would be beneficial to separate them. In this case, microservice architecture provides a clear solution by decoupling the parts of the system.
Microservices provide the following benefits to developers:
- Faster compilation and build time: Faster build and compilation time may play a key role in speeding up all development processes.
- Faster deployments, lower deployable size: When each part of the system is deployed separately, the deployable size can get so significantly smaller that individual deployments can take just a fraction of the time compared to monolithic applications.
- Custom deployment cadence: The microservice model solves the problem of following a custom deployment schedule. Each service can be deployed independently and follow its own schedule.
- Custom deployment monitoring: Some services can perform more critical roles in the system than others and may require more fine-grained monitoring and extra checks.
- Independent and configurable automated testing: Services may be configured to perform different automated tests as a part of the build and deployment pipeline. Additionally, the scope of checks can be reduced for individual microservices, that is, we don't need to perform tests for the entire application, which may take longer.
- Cross-language support: It is no longer required to run an application as a single executable, so it is possible to implement different parts of the system using different technologies, finding the best fit for each problem.
- Simpler APIs: Fine-grained APIs are one of the key aspects of microservice development and having clear and efficient APIs helps to enforce the right composition of the system.
- Horizontal scaling: Microservices are easier and often cheaper to scale horizontally. Monolithic applications are usually resource-heavy and running them on numerous instances could be quite expensive due to high hardware requirements. Microservices, however, can be scaled independently. So, if a particular part of the system requires running on hundreds or thousands of servers, other parts don't need to follow the same requirements.
- Hardware flexibility: Splitting an application often means reducing the hardware requirements for most parts of the system. It provides more flexibility in choosing the hardware or cloud providers to execute applications.
- Fault isolation: Service decoupling provides an efficient safety mechanism to prevent major issues on partial system failures.
- Understandability: Services are easier to understand and maintain due to lower code base sizes.
- Cost optimization: Running most application components on lower-grade instances compared to expensive high-resource monolithic instances may result in significant cost savings for the company.
- Distributed development: Removing the coupling between the components helps achieve more independence in code development, which can play an important role in distributed teams.
- Ease of refactoring: In general, it is much easier to perform refactoring for microservices due to the lower scope of changes and independent release and testing processes, which helps detect possible issues and reduce the scope of failures.
- Technological freedom: With microservice architecture, it is much easier to switch to new technologies given that each service is smaller in size and is structurally independent of the others. This can play a key role in companies with an open and experimental development culture, helping find the right solutions for particular problems and keep their technological stack up to date.
- Independent decision-making: Developers are free to choose programming languages, libraries, and tools that fit their needs the best. This does not, however, imply that there should be no standardization, but it is often highly beneficial to achieve a certain degree of freedom for distributed decision-making.
- Removing unnecessary dependencies: It is easy to miss detecting unwanted dependencies between the components of a monolithic application given the tighter coupling of the components. Microservice architecture helps you notice unwanted dependencies between components and restricts the use of certain services to particular parts of the application.
As we can see, microservices bring a high degree of flexibility and help to achieve a higher level of independence between the components. These aspects may be instrumental to the success of a large development team, allowing them to build and maintain independent components separately. However, any model comes at its own cost, and in the next section, we are going to see the challenges you could face with a collection of microservices.
Common issues of microservices
As with any solution, microservice architecture has its own issues and limitations. Some issues with microservice architecture include the following:
- Higher resource overhead: When an application consists of multiple components, instead of sharing the same process space, there is a need to communicate between the components that involve higher network use. This puts more load on the entire system and increases traffic, latency, and I/O usage. In addition, the total CPU and RAM are also higher due to the extra overhead of running each component separately.
- Debugging difficulty: Troubleshooting and debugging are often more difficult when you deal with multiple services. For example, if multiple services process a request that fails, a developer needs to access the logs of multiple services in order to understand what caused the failure.
- Integration testing: Separating a system requires building a large set of integration tests and other automated checks that would monitor the compatibility and availability of each component.
- Consistency and transactions: In microservice applications, the data is often scattered across the system. While this helps to separate the independent parts of the application, it makes it harder to do transactional and atomic changes in the system.
- Divergence: Different services may use different versions of libraries, which may include incompatible or outdated ones. Divergence makes it harder to perform system upgrades and resolve various issues, including software vulnerability fixes.
- Tech debt addressability: It is much harder to address tech debt in a distributed system where each component is owned by a different team.
- Observability: Managing multiple applications brings additional challenges in collecting and using the system events and messages, including logs, traces, and metrics. Developers need to make sure all such signals are collected for all applications and are available for analysis, including all necessary contextual information to debug any issues and locate the root cause of the issue among the target services.
- Possible duplication, overlapping functionality: In a highly distributed development environment, it is not uncommon to have multiple components performing similar roles in the system. It is important to set clear boundaries within the system and decide in advance which particular roles the components are assigned.
- Ownership and accountability: Ownership becomes a major aspect of the development process when there are many different teams maintaining and developing independent components. It is crucial to define clear ownership contracts to address the development requests, security and support issues, and all other types of maintenance work.
As we have just illustrated, the microservice model comes at a cost and you should expect that you will need to solve all these challenges at a certain point. Being aware of the possible challenges and being proactive in solving them is the key to success – the benefits that we have described earlier can easily outweigh the possible issues.
In the next section, we are going to summarize when to use the microservices and learn some best practices for working with them.
When to use microservice architecture
We have covered the benefits and common issues of microservices, providing a good overview of the applicability of using the microservice architecture model in the application. Let's summarize the key points of using the microservice model, which are the following:
- Don't introduce microservices too early: Don't use the microservice architecture too early if the product is loosely defined or can go through significant changes. Even when developers know the exact purpose of the system, there are high chances of various changes in the early stages of the development process. Starting from a monolithic application – and splitting it over time once there are clearly defined business capabilities and boundaries – helps reduce the amount of work and establish the right interfaces between the components.
- No size fits all: Each company is unique and the final decision should depend on many factors, including the size of the team, its distribution, and geography. A small local team may be comfortable working with a monolithic application, whereas a geographically distributed team may highly benefit from splitting the application into multiple microservices to achieve higher flexibility.
Additionally, let's summarize the best practices of using the microservice architecture model for applications, which are the following:
- Design for failure: In a microservice architecture, there are many interactions between the components, most of which are happening via remote calls and events. This increases the chance of various failures, including network timeouts, client errors, and many more. Build the system thinking of every possible failure scenario and different ways to proceed with it.
- Embrace automation: Having more independent components requires much stricter checks in order to achieve stable integration between the services. Investing in solid automation is absolutely necessary in order to achieve a high degree of reliability and ensure all changes are safe to be deployed.
- Don't ship hierarchy: It is a relatively common practice to split the application into services based on the organizational structure, where each team may be responsible for its own service. This model works well if the organizational structure perfectly aligns with the business capabilities of the microservices, but quite often this is not the case. Instead of using a service-per-team model, try to define the clear domains and business capabilities around which the code is structured and see how the components interact with each other. It is not easy to achieve perfect composition, but you will be highly rewarded for it.
- Invest in integration testing: Make sure you have comprehensive tests for the integrations between your microservices performing automatically.
- Keep backward compatibility in mind: Always remember to keep your changes backward compatible to ensure that new changes are safe to deploy. Additionally, use techniques such as versioning, which we are going to cover in the next chapter of the book.
At this point, we have covered the key aspects of microservice development, and you have learned its benefits and the challenges you may face. Before we proceed to the next chapter, let's cover one more topic to ensure we are ready for the journey into microservice development. Let's get familiar with the Go programming language and its role in microservice development.
Role of Go in microservice development
Over the last decade, the Go programming language has become one of the most popular languages for application development. There have been many factors contributing to its success, including its simplicity, ease of writing network applications, and an ability to easily develop parallel and concurrent applications.
Additionally, the larger developer community has played a key role in raising its popularity across all types of developers. The Go community is welcoming to everybody, from people just starting their journeys into programming to seasoned experts with decades of experience building different types of applications.
The Go standard library provides a set of packages that can often be enough for building a complete web application or an entire service, sometimes without even requiring any external dependencies. Many developers have been fascinated by the ease of writing applications and tools performing network calls, data serialization and encoding, file processing, and many other types of common operations.
This simplicity, paired with fast and efficient compilation into native binaries as well as rich tooling, made it one of the primary languages for writing web tools and services. The high adoption of the Go language for web service development made it one of the primary choices for writing microservices across the industry.
The biggest advantages of Go for microservice development include the following:
- Smooth learning curve: As one of the critical aspects of application development in growing teams, the simplicity of the Go language helps reduce the onboarding time for new, inexperienced developers.
- Explicit error handling: While being a hot topic in the Go community, error handling in Go encourages explicit handling of all application errors. It aligns with one of the key principles of microservice development of designing applications for failure.
- Useful standard library: The Go standard library includes lots of packages that can be used in production-grade systems without requiring external solutions.
- Community support: The Go community is among the biggest in the industry and the most popular libraries get enough support and maintenance.
- Ease of writing concurrent code: Concurrent calls are very common in microservice application logic - microservices often call multiple other services and combine their results. Writing concurrent code in Golang can be a fairly trivial task when utilizing the built-in sync package and core language features, such as channels and Goroutines.
The growth of the Go community has increased the rate of development of additional libraries for the language and resulted in the creation of the entire ecosystem of tools, powering application logging, debugging, and implementations of all widely used networking protocols and standards. The community keeps growing and the rate of new releases is only accelerating.
We have discussed the key aspects of the microservice development model, including the motivation to use it, the common benefits, and the possible challenges. You have learned that the microservice model brings many advantages, helping you achieve a higher degree of flexibility. It also comes with its own costs, which often include additional complexity and a lack of uniformness in the system. Microservice architecture requires you to think about these problems proactively in order to address them before they become big issues.
In the next chapter, we are going to start our journey into microservice development with the Go language. You will learn the important basics of the Go programming language and we will scaffold our microservices, which we are going to improve throughout the rest of the book.
- A collection of resources on microservice development: https://microservices.io/
- Overview of the microservice architecture model: https://martinfowler.com/articles/microservices.html
- 15 best practices for building microservices: https://www.bmc.com/blogs/microservices-best-practices/