Dilemma on Utility Modules: Making a JAR or a Separate Microservice?
Learn about whether to make a JAR or a new service when writing a utility module in your microservices.
Join the DZone community and get the full member experience.Join For Free
In my previous article, I talked about how you can come to a conclusion about what to choose for your new project: microservices or a monolith.
As an architect, if you followed the points in the previous article and came to the conclusion that you will use microservice architecture, cheers to you. You promoted yourself as a first-class citizen of the new era of he digital world
But what's next? You've heard that microservice architecture demands componentization of services, but what actually is a component in the microservice world?
In this article, I briefly discuss what componentization means and when we need to do componentization of a utility module, what problems we face.
Componentization of Services
We understand that a microservice is a suite of small services, so the main objective is breaking a project into multiple services. But what does it mean? Which kind of services are those? Are we talking about a service layer in a layered architecture, or a service wrapped into the JAR's so-called "library," or publishing it through a REST API?
To understand, always remember: microservices means an independent thing (component/service) that can be deployed, updated, its lifecycle managed independently.
When you create a microservice, be very clear about that — there should not be any confusion. Many tricky situations may come, but stick to the basics: "microservice" means an independent deployable thing.
One of the best dilemmas is, suppose you have a utility module. Now you are confused about what to do: wrap the utility module into a JAR and have all other microservices use it as a JAR so that there won't be code duplication, or expose a utility service with an API fiction (a separate microservice) so others can consume it?
One may think that making a utility module a JAR seems severely wrong in the microservice workspace, as "microservice" means several independent small services; the utility module should be published as a service, so the utility module must be a microservice.
Why a Utility Module as a JAR Is a Bad Idea
When you import a utility module as a JAR, you limit yourself. Now your service is dependent on a utility module which packaged as a JAR. If the utility module's Java version upgrades, you have to upgrade your service or your program will not be able to use that JAR. Now you both should stick with same language, Java (although once service is written in one language, I rarely saw it rewritten in another language). Having said that, you should not confine your microservice to a language (Java). Your utility module is used by many microservices as a JAR, so if you want to upgrade your version, it should be backward-compatible. Say you want to create functions which can easily be achieved by Java 9, but you can not upgrade your utility as other microservices are not upgraded to Java 9.
Why a Utility Module as a JAR Is a Good Idea
As counter-logic, many could argue that to build a microservice itself, we import JARs/libraries like Sprint Boot starter parent, GSON, or Jackson, so why shouldn't we package our utility as a JAR? Why is it a bad idea?
Let me tell you, many architects think it is a brilliant idea as it solves many problems. If we are not using a utility module as a JAR then we have two options:
1. Duplicate the functionality of a utility module in all microservices.
2. Create a microservice called "Utility service" and publish an API to invoke utility methods.
These two options have their own demerits.
1. Duplicate the Functionality of the Utility Module in All Microservices
Here, the objective is to duplicate the code to all microservices who consume the utility module so there is no utility module as such, everything is part of the local codebase, but it is against the DRY principle and is a bad idea. If the utility module is holding complex code which associates many classes, it's necessary to copy that to all microservices, and any future changes will have to be copied over all microservices.
This leads to a problem of maintainability. Say we have a utility module which calls an external service and get a complex response. Then, this module parsing the responses applies business logic to it and creates analytical data which is exposed as public methods (Java API).
Now if you copied that complex algorithm written in the utility module into multiple services, certainly, it would be a very bad idea. Think how many times duplication has been done, and if now, new analytical data is needed, you have to add it into all microservices. What a pain — while I am writing this, I am getting afraid to imagine the scenario. So, certainly, it does not work unless you have a very small portion of utility code which is not changeable, say, generic code which deals with Date, TimeZone, and Format (a single class with multiple static methods). Copying that class in all microservice is a one-time effort.
2. Create a Microservice Called "Utility Service" and Publish an API to Invoke Utility Methods
So, you create one microservice where all utility methods are dumped. As this is a utility module, all or most of the microservices communicate with it, so every microservice is linked to this microservice. Just imagine the picture on every microservice dependent (HAS-A); if that service is down due to some erroneous code or all instances are down due to major causes, all services will be down. Then the whole microservice architecture is doomed, so how is it different than a monolith?
As per microservices, a partial failure may happen, but all the microservices will not ever be down — they have 100% uptime.
So, creating a utility microservice is not looking very promising.
As an architect, what should you do now? It is like a double-edged sword — whatever option you choose, you have to deal with disadvantages.
Making a Decision
As an architect, we always deal with disadvantages and try to choose in which case has the fewest disadvantages. So, as an architect, our favorite answer is "It depends on the scenario," and we are hated for that answer, even juniors mocking us.
Here. I will also give the answer in the same way: it depends. It depends how your utility method is written — in a one-liner, if your utilty functions are stateless, use a JAR. If your utility functions are dealing with state, use a microservice.
If you observe, you can divide your utility methods into two categories. One type of utility module takes an input, does an operation on it, and returns a result, so there are no side effects and it is independent of any parameter state. Every time you pass the same parameter, you get the same result. In this case, do not publish those as a separate microservice, as this is not dealing with state. There's no need to publish them as a REST resource because no Create, Put, or Delete operations are being done. You can package this as a JAR and use it in every microservice. Think about utility JARs like Jackson and Gson. They take a request, do an operation, and return data structure, but if your utility requests to fetch data from the database, do an operation, and return it or save that state in the database, then it would be ideal to publish it as a microservice.
Also, think about failure. If you make a synchronous operation, then your utility module failure means the whole chain of microservices fails. Think whether you can make the utility operation async — does the utility module need to be sync? As an example, an event store or store audit information: those are cross-cutting concerns and a utility operation, so it is not a part of the main business flow. Therefore, we can make that async so that if audit information or even a storing operation fails, it does not block the business flow and your service should not go down because of it.
If you have to use a utility operation in a sync fashion, implement a circuit breaker and return a default path so that if the operation fails, it does not block all microservices; it can at least show a default path and show the user a message. Or, users can perform other operations rather than this function. Say your transaction function calls a utility function to check the transaction amount, and based on that, the bank gives you some credit points. Now, if that function fails, we can show a default message like "At this time, we can't process transaction," but the user can do other options, like balance a check. At any moment, the entire service will not be down.
There are also different shades of utilities. Some are mostly read-only but have database operations, some may use in-memory caching, so make your decision wisely.
Published at DZone with permission of Shamik Mitra, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.