Poyters
Published on

Microservices in a nutshell

Authors

Updated at 6/27/2024

Today, we will explore the captivating realm of microservices architecture! With substantial experience in designing systems based on microservices, I'm excited to share insights and practical tips with you. Over the years, I've gained invaluable expertise in crafting and implementing extensive systems.

A significant credit goes to "Building Microservices" by Sam Newman1. This book not only inspired me to explore the intricacies of microservices but has also proven to be an indispensable guide through challenging project endeavors. I wholeheartedly recommend it to anyone intrigued by the topic! Join me in unraveling the fascinating world of microservices.


When to choose microservices?

Here are some situations where choosing microservices might be beneficial:

  1. Scalability requirements: Microservices allow for independent scaling of different services based on their individual needs.
  2. Technology heterogeneity: Different parts of the system may need to be written in a vary technologies. Microservices architecture provides an easy way to make it approchable. 🛠️
  3. Autonomous development and Deployment: Microservices enable independent development, testing, and deployment of services. This autonomy can be beneficial for teams working on different services concurrently, allowing faster release cycles for individual components. 🚀
  4. Frequent updates and Continuous Delivery: Microservices facilitate continuous integration and continuous delivery (CI/CD) practices. If you need to release updates frequently without affecting the entire application, microservices can help achieve this goal.
  5. Fault isolation: Microservices offer better fault isolation, meaning that if one service fails, it doesn't necessarily bring down the entire application. This can contribute to better system resilience.
  6. Complex domain: For applications with a complex and large domain, breaking it down into smaller, manageable services can make development, testing, and maintenance more manageable.
  7. Resource efficiency: Microservices can be more resource-efficient as each service can be deployed and scaled independently, optimizing resource usage based on demand. ⚙️💡

Do not use microservices!

At this point you may admit that microservices are the best architecture. But no, I strongly advise against using microservices if you really don't need them. Microservices architecture (as a distributed architecture) is one of the most difficult to write and maintain. Especially when the team is small and inexperienced.

Microservices architecture is brilliant and most powerful in certain cases. It should be an evolution of the system architecture rather than a starting point.


One big problem about microservices

Microservices architecture has one big problem: it is often written in the wrong way. In the next lines of this article, we will examine the most common mistakes associated with implementing microservices, such as coupling.


Coupling

In the world of microservices, overall coupling is like figuring out how much each service relies on the others. Keeping it low is the secret sauce for letting each service shine on its own — scaling up, doing its thing, and staying hassle-free. ✨💃🕺

Remember: There will always be some level of coupling, so don't be afraid of that! You have to make some trade-offs and decide what is best for your project.

Data Coupling

Data coupling, a prevalent aspect in microservices architecture, arises when microservices share data. When one microservice relies on a data structure or content created by another, changes in data format or content can affect multiple services. An example would be when multiple microservices use the same database.

Picture this: you have a user data set in database. It's used by microservice A and B. In case of adding a new field to database schema it the best case you need to update both schemas in the both microservices or worse, forgot do do that, and not updated microservice can just stop working (especially when you are using strongly typed database)

*You can push a schema to another library and share it between A and B microservices. I highly not recommed such approach. This requires to maintain another library and it's harder to see a border between these microservices.

Ideally, each microservice should have its own database, as in the diagram below:

But! Sometimes this can be inefficient or you can't deal with additional databases. In such a case, it can be quite a good approach to create another service (usually called DAS - Data Access Service), which is responsible for communicating with the database and only does it.

Domain coupling

Domain coupling comes into play when microservice A needs to connect with microservice B because B relies on the functions offered by A.2

Typically, domain coupling is a weak coupling, so it's not so dangerous to our architecture. However, when a microservice finds itself interacting with numerous others, it hints at a scenario where a significant chunk of logic is centralized. This situation could potentially impact the intended benefits of a distributed and independent microservices architecture.

Temporary coupling

Temporary coupling occurs when microsevice A needs to pass data through microservice B, so that microservice C can read it. We can compare it to prop drilling. Such coupling destroys the independence and hermetization of microservices.


Dividing

Dividing serves as a fundamental strategy for breaking down monolithic systems into manageable microservices. This pivotal step involves dividing the system's functionalities to create independently deployable and scalable services. As we embark on this exploration, we'll dive into various techniques, each offering a unique perspective on how to effectively partition a system and how we can define the boundaries of our microservices.

Dividing by change

You don't want to implement a huge part of the system just when you have changed a piece of specific logic. That's what dividing by change is all about. You can highlight which parts of the system need a lot of changes and which parts are rather outdated. You can separate them by simply moving the frequently changed part to a new microservice - this approach provides better separation for frequently changed/expanded logic, which is more dangerous and less vulnerable.

Dividing by scalability

When you're breaking down your system into microservices, think about the parts that could use a speed boost or need to handle more action. Consider the areas in your system that could use a bit of a performance upgrade or need to be able to scale up on their own.

To save some money on scaling, it's a smart move to shuffle all the heavy-duty tasks into their own microservice. This way, you can tweak the performance of these parts separately, making your whole system more flexible and cost-effective when it comes to handling more workload.

Domain Driven decomposition

DDD encourages a strategic approach to dismantling complex monolithic systems by viewing them through the lens of the business domain. Bounded Contexts become the blueprint, defining clear boundaries and ensuring each microservice encapsulates a distinct facet of business logic. This decomposition by subdomains allows for the creation of focused, cohesive microservices, each contributing a unique and valuable business capability. The move away from shared databases promotes autonomy, enabling each microservice to manage its data independently. As we embark on this journey inspired by DDD, we reshape not only our technical architecture but also align our systems with the true essence of our business, fostering a more agile, scalable, and purpose-driven microservices landscape.


When we should combine microservices

Simply put, if updating one microservice necessitates updating another, it's a sign that the service boundaries may need reevaluation. The goal is to have independent, loosely coupled microservices, and dependencies between them may indicate a need for adjustment.


Versioning

At first: remember to use SemVer. It's a global standard and you should stick to it.

Avoid versioning

Let's talk real talk – versioning APIs introduces complexities, leading to fragmented codebases, increased maintenance overhead, and potential confusion for developers and users alike.

The idea is to prioritize simplicity whenever feasible – embracing microservices architecture from the get-go, where possible, to sidestep potential pitfalls like codebase complexities and unnecessary fragmentation. By fostering a mindset of continuous improvement and maintaining backward compatibility, we create an environment where versioning becomes more of an exception than the rule. There are instances where versioning becomes a lifeline, it's truly necessary and needed - in such case we should do versioning carefuly and make it as simple as we can.

Keeping it simple

Picture this: You find yourself in a situation where a breaking change in one API endpoint is inevitable. Despite the challenge, let's explore how we can handle it with simplicity in mind. There are a couple of strategies we can employ:

  1. URI Versioning: create a new endpoint, mirroring the previous one: /api/user becomes /api/user/v2. This approach distinguishes versions directly in the URI.
  2. Header Versioning: alternatively, you can add the version number to the header. This method keeps the URI unchanged, and version information is included in the HTTP header.

In both cases, these versions need to coexist. When introducing the latest endpoint, make necessary interface and logic changes, but strive to reuse as much code as possible from the previous endpoint. Limit modifications to the essential components, ensuring a more straightforward maintenance process for both versions. It's all about efficiently managing changes while keeping the codebase as clean and maintainable as possible.

*The adapter design pattern can be beneficial in this situation.

API Gateway Versioning

Imagine this scenario: You're in the midst of a major transformation—overhauling architecture, adopting cutting-edge technologies, and introducing a slew of innovative functionalities. As part of this evolution, there's a critical decision to create a new version of your API, treating it as a standalone project. This strategic move not only positions you to gracefully retire the current version eventually but also ensures the ongoing maintenance and support of the latest iteration.

Enter API Gateway versioning, a powerful ally in this journey. By configuring a special URL, say /api/v2/, through the API gateway, you gain the ability to seamlessly redirect traffic between the old and new APIs. It's like having a dedicated traffic cop, directing requests intelligently based on specific rules.

Tailoring these rules can even extend to different domains, allowing for a personalized redirection experience—for instance, directing traffic from one domain (client, e.g. A) to v1 while smoothly transitioning another to v2. 3

This dual-project approach not only accommodates the evolving needs of your current system but also sets the stage for a future-ready API landscape. It's a dynamic strategy, combining the wisdom of retiring the old with the agility of embracing the new, all orchestrated through the strategic prowess of API gateway versioning.


Pros and cons of microservices

Let's sum it all up and see the good and bad points of microserfices architecture.

Pros

  1. Seamless technology switching: enables smooth transitions between technologies, allowing for the adoption of the most suitable tools for each microservice.
  2. Tailored technology and team alignment: facilitates a more precise alignment of technology and team expertise, allowing for optimal matches tailored to the specific needs of each project or microservice
  3. Independent and safer deployments: supports independent deployment of microservices, enhancing flexibility and ensuring a more secure release process.
  4. Scalability: allows for efficient scaling of individual microservices based on their unique requirements, promoting resource optimization.

Cons

  1. Steep learning curve: demands a deep understanding of the architecture, making it challenging for teams without the necessary expertise.
  2. Increased maintenance time: requires additional time for maintenance due to the decentralized nature of microservices, potentially leading to higher overhead.
  3. Enhanced discipline needed: imposes a need for strict discipline in development, testing, and deployment processes to ensure the effective coordination of distributed microservices.
  4. Overhead of communication: introduces the need for effective communication between microservices, which, if not managed properly, can lead to increased complexity.

Summary

Let's start with the fact that you probably don't need a microservices architecture. This approach and the desire to maintain a "pure microservices architecture" can lead to wasting a lot of time maintaining the architecture. In most cases, you probably need something called a three-tier monolith (or modular monolith). And remember that the architecture should grow with the project. Microservices should not be a starting point, but an evolution of the project.


Additional resources


Last words

That's all about microservices. Thanks for reading! If you enjoyed it, I will be very pleased if you follow my GH account: https://github.com/RafalKostecki

Footnotes

  1. Buy at least second version - Building microservices

  2. To work properly microservice A needs to use microservice B. Picture another situation: Microservice A, C and D needs microservice B to work properly.

  3. Client (api consumers) A and B uses the same URI but API Gateway configuration determines that client A uses API v1 but client B uses API v2