In many companies nowadays, microservices is the de facto way of handling service architecture.
Some do it out of necessity as their application has reached a scale where the monolith is a bottleneck. Others, simply like being onboard the hype train.
Whatever the scenario, the decision is often backed by the classical case for adopting microservices, which every junior dev studies extensively before their system design interview.
What gets often neglected, however, is the problems which come with such an approach.
Each of these problems usually demands a sophisticated solution, which raises system complexity.
One such problem is how to reuse the shared infrastructure components in your microservices. Each of your services will probably have a distinct business logic, but it will also come with a big baggage of infrastructure code.
These components usually don't change too much between your services - healthchecks, monitoring configs, logging, standard service configurations, etc.
Fortunately, there is a very elegant solution for this problem for your Go services, which utilises the Fx Framework. It helps you by automatically managing your dependencies, but it can do much more than that as I'll show you in the upcoming sections.
In this article, I will show you how to effectively extract your components into reusable & independent modules which can easily be shared across your Go services.
Make Sure You've Covered the Basics First
I've already covered the basics of Dependency Injection Frameworks and Fx in this article. In it, I've shared how to extract your code into separate functions, which can be automatically wired using the framework.
In this article, I will continue from where the previous one ended. If you want to get the full picture of what we've been doing, I suggest going through that article first.
Alternatively, if you're already familiar with DI frameworks and are only interested in the problem outlined by this article, then get familiar with the code here. We will use that as a baseline, which we'll refactor.
Supplement your code walkthrough with the linked article, which explains some of the primitives used there.
What We've Got So Far
We're starting from this small web application, which simply displays a "Hello World" message when you access localhost:8080.
The application has some infrastructure code already. What I refer to as infra code is (in this case) reading the standard config file, initialising the logger & serve mux.
It is that part of the application which isn't related to its business logic but is necessary for managing your service properly.
The codebase we're starting from is already in good shape as we've abstracted away the wiring code necessary to wire up your components and their various dependencies.
However, we still have everything bundled in main.go
, instead of it being organised into distinct components.
This is what we'll focus on in this article.
As a side-note, have in mind that the code in this example application is purposefully kept simple in order to be easier to digest & follow along.
In production microservices, the components will be much more hairy and contain much more details.
Nevertheless, this article will cover all the concepts you'll need in order to apply them to your production microservices.
This is a trade-off I've made in order to balance readability & completeness.
Refactoring our Application into Fx Modules
In order to standardise how components are being shared, we will bundle a component's provided functions into modules.
Our modules are logical units which encapsulate the functions provided to Fx.
I will walk you through how to do that in the next sections.
If you're not interested in the process of getting there, but simply want to look at the final result, check out the final code here.
Creating our Configuration Module - configfx
We'll start by refactoring this piece of code, which provides our configuration-handling code to fx.
It contains a bunch of yaml struct definitions, used to parse our standard configuration:
And it continues with the actual provider function which reads the config, parses its contents and is finally provided to fx:
To decouple this from our main.go
file, create a new package called configfx
and a go file configfx/configfx.go
and move the function there:
Now, in the same file, we will bundle the provided function in an Fx module like this:
Finally, import the new package in main.go, delete the old code and provide our brand new module:
This final step of bundling the ProvideConfig function in an exported Module variable seems redundant, but it helps in two ways:
- In reality, modules often provide more than one function.For example, in this case, we could easily provide the separate configuration sections in different provider functions.This avoids client code having to import our entire configuration when it only needs one of all the sections. I haven't done that here to keep the code simple & easier to follow.
- It makes code more readable and standard by following the convention of exporting a single Module variable in all components
This covers the configuration section. The rest of the transformations are pretty similar.
Creating our Logger Module - loggerfx
This step is pretty similar to the previous one, we'll take our logger initialisation code from main.go
:
And extract it into its separate loggerfx
package in loggerfx/loggerfx.go
:
Finally, provide the new module in main.go
:
This transformation was pretty similar to the previous one, so there is nothing too interesting to discuss here.
Let's move on to the more interesting cases.
Creating our Http Module - httpfx
This transformation is very simple, but holds the potential to plug-in very interesting things.
Take the provided http.NewServeMux function from main.go
:
And extract it into a httpfx
package in httpfx/httpfx.go
:
This was probably the simplest transformation, but it provides the most interesting opportunities.
In this module, you can plug-in any behaviour, which is standard to your http microservices without the consent of any of your other components.
For example, let's say you have a set of standard http endpoints which should be enabled by default in all your microservices.
A great example is the /health
endpoint which should unconditionally return 200 OK
.
This is your health check endpoint which should be present on all your services.
Perhaps some of your dashboards invoke it for all your services to monitor if your service is alive.
It would be very tedious to make the service developers have to write that code every time.
What's more, you're taking a risk by relying on your developers to do that in each service.
Doing it manually for each service is error-prone.
This is just one example, you could use this package for.
There are many more - adding a standard auth middleware, adding a standard logger for all HTTP requests, providing a similarly configured http client, etc.
Connecting The Dots - bundlefx
By extracting all our providers into separate Fx modules, we've achieved a very lean main.go
file:
This is already a great place to be, but we can put one final touch to this application, by bundling all the infra components into a single package, called bundlefx
:
After we do this, our main.go
file is pretty slim:
Notice how even the hooks can be extracted in a separate independent module. This allows you to entirely disregard how your server is being boot up or ran.
All you need to do is register the routes you need. bundlefx
will take care of the rest.
How to Leverage This In Your Microservices
Notice how all our infra code is now in a single module, which we can extract into a separate repository & import is as a library via go modules
.
Using this approach, you can now supply this package to all service developers in your team. They simply provide it to Fx and get all the standard machinery your service needs.
What's more, their service source code will now comprise only of that service's business logic, while supporting the necessary infra for your specific environment.
If your needs grow in the future and you need e.g. a new standard endpoint, which you want to ship to all services, simply update the bundlefx
library, ask service developers to update the library to latest version and voila.
There are some caveats to look out for, though.
Since you're hiding all the infra details from your service developers, they won't know what options they have available.
This is why, you should make sure that all your standard packages are very well documented, by providing a reference guide to exported objects & examples on how to use them.
Additionally, some might be afraid that bundling all your infra in a single package like this will make your services unnecessarily heavy-loaded as they might not need all of the standard packages bundlefx
supports.
This, fortunately, is not the case as Fx only provides those objects which you need. For example, if you never request a *zap.Logger
in any of your components, Fx will never invoke the functions in the loggerfx
package.
Conclusion
Structuring your services into Fx modules enables you to create reusable & independent modules, which can be easily composed together & reused throughout your microservices.
This is especially useful for extracting common infra code into a separate package, reusing it across your services seamlessly.
Doing this, will enable your team's service developers to focus on their business logic, without spending extra development time on wiring infra code across your microservices.
It will also enable you to independently evolve your platform's infrastructure without affecting the existing service code.
Top comments (0)