A lot is being said about decoupling systems from each other in software development. Nobody likes to be blocked by another team where you have limited control over. Decoupling helps with this issue by making only the interaction between multiple components shared, while keeping the internals hidden from the outside world.
Developers also found out that decoupling isn't only handy when dealing with overlap between multiple teams, but even in a single team, having overlap might make things more complex. Heavy dependencies on a certain component in your business logic, might make it harder to swap that component for something else later on.
Both are valid use cases for decoupling. Both define cases where you do not want your components to be a singular monolith, where no single brick can be swapped for another. Decoupling helps with making the bricks hot-swappable, without having the entire structure tumble down.
I've talked about decoupling in the last paragraph & why it can be useful. I have not talked about how to implement it. Most programmers, especially in the backend-space, will instantly shout "Microservices are the best way to decouple your components!". To a certain level, I agree, but let me first introduce what they are on an abstract level, before I go over reasons not to use them.
Microservices are small nodes of computation, each node having its own endpoints as interfaces & acting as a micro-webserver. A microservice on its own has no value, as it only defines very basic operations in, in most cases, a singular business domain. Obviously, with a single business domain, you cannot build an application.
Most microservices are built in a type of mesh, where one microservice can communicate with another. Another type of connection is to the API gateway, which connects the outside world, with your inner mesh of microservices. This gives you the ability to connect your external interfaces with a reaction-chain internal to the microservice mesh.
Taking this into account, we immediately can list a few benefits:
- Very simple components that can be easily connected into a chain of operations.
- Easier ownership of each business domain, as they are linked to one or more microservices.
- API gateway hides the inner complexity of your microservice chain & mesh.
There are thus quite a few upsides of using this architecture, especially in larger teams. Of course, we also need to highlight some downsides:
- The chain of operations can become quite complex if undocumented. The fact that you use a loosely coupled system, means that the history of the operation chain is lost if not actively kept.
- Communication between servers is faulty by nature. gRPC tries to solve most of these issues, but can't hide all of them.
In this article, I want to propose another solution of the second issue, mainly to be used for sharing logic in team-internal systems.
Let me start with taking a step back before we dive deeper in the other proposed solution and why it works. I would first like to start with explaining what a package is. Later during this article, I'll make the jump back to our initial problem of decoupling.
A package is in most cases something that contains a few different items, but encapsulates the entirety of those items in a single whole. In distribution, this can be a carton box. In sales, this can be a collection of features to be sold for a certain price. In software engineering, this is in most cases a collection of functions that are collected and boxed to be used somewhere else.
In practically every software project, you depend on things other people implemented & exposed to the public. Github is full of open source projects, some of which you have undoubtedly used in your own projects. Think of React.JS, Gunicorn, FastAPI, Lombok, ... Each of these projects needs to package the functions & utilities of their implementation for use by others.
A few benefits:
- Code running in this way is an integrated part of your own logic, making it easier to debug, jump around in the code and possibly overwrite certain behaviour
- Implementation of new versions can happen without the necessity to upgrade immediately due to semantic versioning.
- Most programming languages have packages as a first class citizen. You cannot, after all, implement everything in your own codebase.
Notice, in the last paragraph, that we defined large open source libraries. Note how you probably have never touched any of those, but are still using them frequently and without issues? Well, this is exactly a way of decoupling logic. In a strict sense, you are still running the given logic on the same machine as the rest of your logic. From a business perspective though, you are fully decoupled from the people implementing these packages. None of them (excluding rare cases) work in your team or even company.
As such, I propose building & reusing packages as another way of decoupling. Thinking of the benefits, you can easily see that it is possible to decouple and organise the implementation of certain business domains easily. As such, let's take a look a the benefits & downsides between using a microservice architecture against a package:
- It's seamless to upgrade your logic without other teams' intervention. For packages, you need to manually update the version to follow the upstream.
- The microservice architecture has more frameworks & buy-in from the development community.
- Each microservice can choose different machine resources for its workload. Some services might need more CPU/Memory/Scaling than others.
- You are running your code integrated in your larger code base. This lowers complexity of the solution, as operation chains and dependencies are easier to follow.
- You are running your code on the same machine, meaning you do not need to think about cross-server communication and request-response flows.
- You can choose when to upgrade your service to a new version, giving you time to upgrade your code at your own pace.
As you can see, the benefits for each are fairly balanced. It is thus a matter of which benefits outweigh the other for your specific application. If your package doesn't add a big load to your machine's resources, I think they are a valid way to increase decoupling in a low effort way.
We started explaining decoupling and why it is such a big topic for software engineering, especially in larger corporations. Keeping your business domains cleanly separated and upholding the possibility to work on each in isolation, helps businesses move rapidly.
In most cases, microservice architectures are used to lower the coupling between components, as only the communication is shared between the different services. This means only interfaces are shared & internals hidden. Of course, this is not the holy grail and there are quite some downsides as well, such as less visibility in both the operation chain & the effects of downstream calls. Complexities of API calls are also not to be underestimated.
We explored the definition of a package and raised that it might be a good alternative to microservices to decouple certain parts of your code. The characteristics of both solutions were put side-to-side to show in which cases packages might make sense for your solution.
Hopefully, this article inspired you to not blindly follow microservice or monolith architectures as the holy grails of development. The landscape of software engineering is as wide as it has depth, meaning there are practically always more solutions than the single one your focus falls on now. Keep an eye out for those as well, as the holy grail is always dependant on the application.