19/03/2023

Microservices Magic: Building Modular and Maintainable Systems

Microservices is an architectural approach, where multiple loosely coupled backend services are developed, deployed, and maintained individually, but work together to solve larger, more complex business problems.

By Andrew Howard in scalable software

Share on facebook Icon Share on twitter Icon Share on linkedin Icon
blog main image

Microservices is an architectural approach, where multiple loosely coupled backend services are developed, deployed, and maintained individually, but work together to solve larger, more complex business problems.

Each service is responsible for its own discrete tasks and communicate with one another via loosely coupled Application Programming Interfaces (APIs). Since each service is loosely coupled, they do not need to concern themselves with the implementation of the other services. Changes to one service should not impact other services unless the interface changes.

This allows for the tasks of a service to be developed and maintained separately, but also allows for the implementation to be swapped out entirely without impacting the other services.

The opposite approach to microservices is a monolithic service, where all tasks are handled by a single service. While a monolithic service’s tasks can be implemented in a loosely coupled manner, the risk of creating tightly coupled dependencies is far greater, which makes the code base more difficult to maintain and increases the risk of regression.

What are the benefits?

For those who have read my previous article: “Micro frontends and their place in large scale enterprises”, you’ll notice I’m re-using the subtitles. This is because microservices and micro frontends aim to solve the same problems. Microservices solve this for the backend, while micro frontends are self-explanatory.

Scalability

Microservices are scalable in two aspects:

As with micro frontends, the development teams can work in relative isolation to one another. They develop and maintain their own code bases, without the friction of too many developers working on a single codebase. This allows more developers to work on the greater picture concurrently.

The other benefit of microservices is that their infrastructure can be scaled independently of one another. To handle high traffic, a service may need multiple instances running concurrently, with load balancers in the foreground to delegate requests to each instance. Scaling a monolith is far more resource intensive than scaling only the microservices that are under load.

Maintainability

As previously mentioned, the loosely coupled nature of these microservices forces separation of concerns. The service becomes a black box. Other services depending on it do not need to concern themselves with the implementation and it is not possible for them to couple themselves tightly to the implementation of another service. This reduces risk of regression.

Flexibility

In some cases, different programming languages offer advantages over others. For example, we’ve found Python to have far better PDF generation libraries that .NET. In a monolithic approach, you are stuck with everything using the same language. With microservices, each service can choose the language and framework that best suits its tasks.

When it comes to hosting, a monolithic service would use the same hosting infrastructure for all its features. With microservices, each can use different infrastructure based on its needs.

Let’s say for example, we need to generate a PDF and email it to a user after they have completed a transaction. That transaction can be performed from a .NET service hosted on some server. The transaction can invoke a PDF generator microservice which is written in Python to benefit from the superior PDF libraries. Additionally, if we do not require a quick response from the PDF generator, the service can be hosted on an AWS lambda for example. This is a serverless approach, which reduces hosting costs. The lambda takes slightly longer to respond from a cold start, but if a quick response is not necessary, the lambda can complete its task in the background and email the PDF once it has completed.

Microservices enable flexibility in both language and framework choice, as well as how the service is hosted, leading to reduced hosting costs.

Reusability

One microservice can be consumed by multiple other microservices, even when these other microservices are not necessarily working towards the same goal.

One example is my current project’s Authorization API. On this project, which involves building a micro frontend platform, we have an Authorization API and an API which handles platform specific tasks such as deployments of the micro frontends and the management of their configs and manifests. The platform API uses the Authorization API to verify the requestor is authorized to perform the requested task.

We are however in the process of building a replacement platform which addresses the shortcomings of the older one. This new platform will have its own APIs, but the Authorization implementation remains the same (albeit with different permissions and scopes). Both platforms’ APIs communicate with the same Authorization API.

Additionally, there is some functionality which is common between the two platforms, which is being implemented as its own microservice. Once implemented, we could update the old platform to consume this functionality from the new microservice instead of handling those tasks itself.

Sounds good. What’s the catch?

No architecture is perfect. They all come with pros and cons. In the case of microservices, the two main challenges are overhead and infrastructure complexity.

Since each microservice is hosted separately, communication between them is not as fast as sections communicating within a monolith on the same host. This can largely be mitigated by hosting them within the same hosting infrastructure, be it a cloud provider or an on-premises Kubernetes cluster and having the services communicate with each other directly as opposed to over the web.

This unfortunately results in more complex infrastructure. Each microservice requires its own hosting infrastructure set up. To communicate with each other directly, additional infrastructure such as security groups may been to be necessary.

Debugging also becomes more challenging, as a single request could touch on multiple microservices. Thankfully, tracing tools such as OpenTelemetry exist which allow us to view the logs of a request from its origin, through every service invoked, up to its completion.

The added overhead and hosting complexity means it is entirely possible to go overboard with splitting tasks into microservices. Careful thought needs to go into what should be grouped together and what should be split.

Migrating from Monolith to Microservice

A monolithic service can gradually be broken up into microservices over time. The complexity of doing so, depends on how tightly coupled the various features of the service are. In the micro frontend project that I mentioned previously, we specifically designed the platform API to be a collection of projects which provide services which are then bootstrapped by a single host. By doing this, we ensured our coupling was always loose, and that the projects could be split into multiple microservices.

Splitting up a tightly coupled monolith is far more difficult. Sometimes, there are quick wins. Consider the example of the PDF generator microservice. This example is derived from a past project where we had a monolithic service. One task was to generate and email a PDF after completing a transaction. The initial implementation did this as part of the same API request. The snag was that PDF generation took long. The result was that the user would stare at a loading spinner while waiting for the PDF to generate and be emailed to them. To address this, we moved the PDF generation to an AWS lambda which was invoked on successful transaction completion. The API would respond to the user after completing the transaction, while the PDF was generated and emailed by the microservice asynchronously in the background. By splitting out this one task into a separate microservice, we were able to make use of better PDF generation tooling, while gaining a significant performance improvement to the remainder of the monolith.

Should every project use microservices?

No. Sometimes the scope of a project does not justify the infrastructure complexity required by microservices. When a team consists of only a handful of developers and the total load on the system is unlikely to scale greatly, the benefits of using microservices over monolith aren’t as noticeable.

That said, it certainly pays to plan for the future by designing the monolith in such a way that is can be split at a later stage should the scope of a project increase as mentioned in the migrating from monolith chapter.

Conclusion

Microservices allow us to build modular services which are easier to maintain than their monolithic counterparts. While there is added complexity in the hosting infrastructure, the code itself becomes far more manageable, as there are less points of interaction due to the loose coupling of the services.

As a business problem increases in scope and complexity, microservices not only allow us to scale the services more efficiently, but also the number of developers working concurrently on various features. These days, it is almost mandatory for large enterprises to make use of microservices to enable scalability.

And while it may not be necessary for smaller projects to implement a microservice architecture from the get-go, structuring your monolith to be easily will split will enable a smoother migration to a microservice architecture should the need to scale arise.

Finally, a monolith does not need to be fragmented into multiple microservices in one big update, but can instead done on a per feature basis where deemed necessary.

Get Started With Full Stack!

Ready to transform your business? Contact us today to discuss your project needs and goals.