Welcome dear reader! In this blog miniseries, I will go over microservices, why they are great, and convince you why you do not need them.
Short Introduction about the author
Over the past few years, I have been working on a few medium-sized projects using microservices. During that time I have built numerous services and extended existing ones with new features. Through this experience, I have developed some opinions which I would like to share with you.
In particular, I have come to appreciate the benefits of microservices and the ability to break down a large application into smaller, independent services that can be developed, deployed and scaled independently of each other. This modularity enables faster development and deployment of updates, as well as more efficient scaling, as resources can be allocated to specific microservices in response to increased demand. I have also seen places where microservices do not perform that well.
Furthermore, I believe that a good microservice should be designed to perform a specific and well-defined task, while also being scalable, reliable, maintainable, and loosely coupled with other microservices. It should be able to communicate effectively with other services in the system and be designed to be fault-tolerant, meaning that it can handle errors and failures gracefully. That’s a lot of words. Don’t worry, we will break down these into bite-size pieces.
History – Microservice
You have probably heard of microservices, but let’s review their history for those who are unfamiliar with them.
The concept emerged in the early 2000s when large companies like Uber and Netflix needed a better solution to manage their codebase. In the traditional monolithic architecture, services are housed in a single large codebase, requiring the whole service to be deployed for even minor changes. Additionally, their services would slow down when applications faced high loads, leading to frustratingly slow websites. Microservices were born from the desire to scale parts of the codebase as needed and to deploy updates to only parts of the service at a time.
Microservices are a broken-down monolithic architecture. This involves breaking down a large application into smaller, independent services that communicate with each other through APIs. Each microservice is designed to perform a specific task or function and can be developed, deployed, and scaled independently of the others.
Moreover, microservices minimize redundancy, as larger applications can utilize the same microservice. Employing microservices also allows for better agility and scalability. Overall, microservices can provide numerous benefits to software development, including modularity, scalability, resilience, security, and observability.
While microservices offer many benefits, there are also costs associated with implementing them. One of the main costs is increased complexity. With a microservice architecture, there are more moving parts to manage and coordinate, which can be challenging and time-consuming. Additionally, deploying and scaling a microservice architecture can be more expensive, as it requires more infrastructure and resources. Finally, there may be costs associated with re-architecting existing applications and breaking them down into microservices. However, despite these costs, many companies have found that the benefits of microservices outweigh the costs in terms of improved agility, scalability, and resilience.
The good – How to Design a Microservice
A good microservice should be designed keeping in mind that it should perform a specific function or task. It should have the capability to scale, be reliable under different circumstances, and should be easily maintainable. A microservice should be designed in a way that it can reliably communicate with other microservices in the system. It should be loosely coupled, which means it should be independent of other microservices. It should also have the ability to be deployed independently. Moreover, it should be designed to be fault-tolerant, meaning that it should be able to handle errors and failures gracefully.
- Modularity: A good microservice should be designed to be as modular as possible, with a well-defined interface and clear separation of concerns from other services in the architecture. This allows the service to be developed and maintained independently of other services and enables it to be replaced or updated without affecting the rest of the system.
- Maintainability: One of the perks of microservices is that they are small, which directly correlates to ease of maintainability. While performing a specific task it should be easy to see what a given service should include or not. It should also be easier to deploy and test as the service is specific to a given task.
- Scalability: A good microservice should be designed to be horizontally scalable, meaning that it can be scaled by adding more instances of the service in response to increased demand. The service should be able to handle large volumes of requests and should be able to distribute the load evenly across instances.
- Resilience: A good microservice should be designed to be resilient to failures, and should be able to recover quickly from errors or crashes. This can be achieved through the use of techniques such as retrying failed requests, using circuit breakers to prevent cascading failures, and implementing graceful degradation when resources are unavailable.
- Security: As with any software a good microservice should be designed with security in mind. It should be able to authenticate and authorize requests from other services in the architecture. Additionally, it should be designed to handle sensitive data securely and protect against common security threats such as SQL injection and cross-site scripting. Microservices should also have a limited view and only access needed parts of a system.
- Monitoring and observability: A well-designed microservice should be easy to monitor and observe. It should have metrics and logs that provide insight into its performance and behavior. This can be achieved through the use of tools such as application performance monitoring (APM) and distributed tracing.
The bad – When a Monolith is Better
Microservices aren’t the solution to all problems. You may be better off using a monolithic approach after all. Here is a list of why you should reconsider going down the microservice path;
- Your project is relatively small: If your project is small in size and complexity, a monolithic architecture may be sufficient to meet your needs. A monolithic architecture can be easier to develop, deploy and maintain than a microservices architecture.
- Your development team is small: A monolithic architecture can be easier to manage with a smaller development team, as there is a single codebase to maintain and test. A microservices architecture can be more complex to manage with a smaller team, as there is some overhead when working with microservices.
- You have limited resources: A monolithic architecture can be less resource-intensive than a microservices architecture, as it requires fewer servers and a smaller infrastructure. If you have limited resources or a limited budget, a monolithic architecture may be a better option for your project.
- Your project has low traffic: If your project has low traffic and usage, a monolithic architecture may be sufficient to meet your needs. Microservices architecture is typically used for high-traffic and high-availability systems, and may not be necessary for low-traffic systems.
Suppose you are developing a small, simple web application that has a few basic functionalities, such as user authentication, data storage, and a basic user interface. In this case, a monolithic architecture approach may be a good choice because:
- Simplicity: A monolithic architecture is simpler to develop and deploy than a microservices architecture. You don’t need to worry about the complexity of managing multiple services, APIs, and data stores.
- Low Cost: Monolithic architectures are often less expensive to develop and maintain than microservices architectures because they require fewer resources.
- Performance: Monolithic architectures can provide better performance than microservices architectures because there are fewer communication overheads between services.
- Easier Testing: With a monolithic architecture, testing is simpler because you only need to test a single application with zero to a few external mocks.
Keep in mind, you can always go microservice later, and there is no one telling you you cannot mix and match. Use the tools that work best for your needs.
Here are some general tips I think one should always follow when building software, regardless of the architecture:
- Modularize your code: Even though you are using a monolithic architecture, it’s important to write modular code that can be easily broken down into smaller services in the future. This can involve breaking down your code into separate components or modules that can be decoupled later on.
- Use standard interfaces: To ensure that your code can be easily integrated with other services in the future, it’s essential to use standard interfaces and protocols for communication. This can involve using RESTful APIs, message queues, or other communication protocols that can be easily integrated with other services.
- Separate business logic from presentation logic: To ensure that your code can be easily reused in a microservices architecture, it’s important to separate your business logic from your presentation logic. This can involve separating your code into different layers, such as a data access layer, a business logic layer, and a presentation layer.
- Use containerization: Containerization is a way of packaging software in a format that can run consistently across different environments. By using containerization, you can ensure that your monolithic application can be easily broken down into smaller services later on.
- Use monitoring and logging: Monitoring and logging can help you identify performance issues and bottlenecks in your monolithic architecture. By monitoring your application, you can identify areas that can be broken down into smaller services in the future.
By keeping these considerations in mind during the development process, you can ensure that your monolithic application is built in a way that can be easily migrated to a microservices architecture.
The Story – Personal Experience
In the real world, it’s hard if not impossible to design flawless services. Here are some things I’ve come over during my work with microservices.
Splitting a monolith – This one sounds simple at first. But quite often the design of a monolith does not go hand-in-hand with splitting it into multiple services. Sometimes we have less than optimal solutions for querying data, and it’s so easy to do a simple join from another database table to get a relationship between two data points. What happens when you split these into their own databases? Will I have to duplicate my data? Or make a custom endpoint for getting the required data?
Testing – It is generally easier to test microservices, but it can become cumbersome when the service is integrated with many other services. Writing integration tests for these services might take longer than writing the actual tests. To counter this a balance between integration tests and unit tests must be made. In contrast, testing a monolithic architecture may be more straightforward, but it can also be more time-consuming and may require testing the entire system, even if the changes are minor.
Debugging – While debugging a single monolithic service is quite straightforward, single microservices can be tricker. To debug a single service, you might need to run multiple services locally, which is less than ideal. It is also possible to debug services in a staging or production environment, but this usually requires some additional tooling. For this, I can recommend Telepresence, a tool that allows you to override a single service with a locally running one.
Service communication – There are various ways to implement communication for microservices. Microservices usually communicate in three different ways: synchronous, asynchronous, and event-driven. Event-driven communication can be achieved with technologies such as Kafka, NATs, and Pub/Sub. Synchronous and Asynchronous communication can be achieved with REST, GraphQL, gRPC, etc.
Communication requires its own part, which is the next part of this miniseries.
Resilience – while microservices are great for resilience since the whole service will not be unavailable if a single part is offline. But how should we handle offline services? This will also be covered in the next part of this miniseries. But in short, we should have a way to handle these situations gracefully, such as implementing retry mechanisms or fallbacks to alternative services. Additionally, it is important to have proper monitoring and alerting in place to detect and respond to failures in a timely manner.
This blog post discusses microservices, their benefits, and how to design them. Microservices are smaller, independent services that communicate with each other through APIs, enabling faster development and deployment of updates, as well as more efficient scaling. A good microservice should be designed to perform a specific task, be scalable, reliable, maintainable, and loosely coupled with other microservices. While a monolithic architecture may be simpler and more cost-effective, microservices provide more efficient updates and scalability. However, designing flawless services can be challenging, and companies should carefully consider their specific needs and requirements before making a decision. Thank you for getting this far!
The next part will cover microservice communication.
Part 1: The Pros and Cons of Microservices: Is It Right for Your Project?
Part 2: Building a Robust Microservice Architecture: Understanding Communication Patterns
Part 3: The Importance of Monitoring and Observability in Microservice Architecture
Part 4: Securing a Microservice Architecture – 5 Pillars
Part 5: Testing in Microservices: Ensuring Quality and Reliability