Objective of this post
The word "plugins" in computer software is about the extension or customization of an application. In this post I want to show you different implementation (or better called thinking) ways of plugins in JAVA with some important code snippets to take away rather than a step-by-step how to implement the plugins (because the parts about Microservices and Cloud service requires many setups behind. You will have the code with you for a full demo). In case you have any question don't hesitate to post it below. Your question may help others
Scenario of the demo application
For better demonstration I will implement the ways mentioned later under the same scenario described below. For each way, the scenario may be changed a little bit but the main idea is unchanged.
There is a game names Happy Farm. This is a game for children to identify animals. We need to implement a welcome screen in the game where a random animal will appear and introduce itself to the player. Since the frontend was developed by another team we only have to care about the backend. For backend we need to implement a method to show a welcome message with the following signature:
String executeCommand(String command);
The implementation should ensure that it's easy to add a new type of animal into application. Currently we only support three types: dog, cat and mouse. Below table is the output of welcome message for each animal corresponding to the input command.
Input command param
Output
DOG~White FangGau gau, White Fang, your loyal servant is here. Nice to meet you, my boss.CAT~TomMeo meo, Tom is here. Do you want to caress me?MOUSE~JerryChit chit, I'm Jerry. Remember to protect your carefully, kaka.
Here are some notes for the imeplementation:
- There will be more kinds in the future. It's planned already but we don't have more details now.
- The command is always in format <animal type>~<animal name>
Solutions
![Solutions](/sites/default/files/2024-05/Java1.png)
Method solution
Implementation
Switch case or multiple if clauses is the easiest and quickest level to implement . Maybe some of you can think of something like this (in the code below Animal is just a DTO with 2 fields name and type. AnimalTypeEnum has 4 fields: UNIDENTIFIED, DOG, CAT and MOUSE)
Sample Method level implementation - OriginalMethodSolutionImpl.java - Expand source
However, everything has a tradeoff. Back to the scenario above you can see that:
- Every time we add a new animal this method is always changed and we will have a new switch case. If we have to support hundreds of animals => hundreds of switch case are added 🤣 => maintenance becomes difficult.
- If the business logic become more complex, e.g. the animal sound must be loaded from a library, of name introduction must handle some exceptions the code becomes harder to read.
- Another point is that this code has a bad smell "Divergent Change": if you need to change behavior of an animal or adding a new animal you always have to modify this method (well, of-course).
- We are duplicated code. Yeah, really, the code structure is duplicated here. If you need to change the general behavior you have to modify all the switch cases => large maintenance effort.
So, if we only have a little time to do and a new animal won't come soon into application, what can we do? Let's have a look at the code below
Lamda approach for method solution - LambdaMethodSolutionImpl.java Expand source
Summary
So, let's compare with the points of the implementation above:
- A little bit better. Well, at least we can split into multiple smaller method and debugging can be easier. However, if we have hundreds of animals, we need to split into tens of method?? 😅 => It's only better but can't solve this point.
- It also can't solve the problem that the business logic become more complex.
- Bad smell "Divergent Change" is still there. We can categorize the animals to add and create multiple methods to add into the map (e.g. grouping dog and cat to pets, mouse to disaster) => the main method is not changed. However, IMHO it's a kind of workaround.
- Code duplication (can understand as structure duplication) problem is still there.
- Before JAVA 8 we have to use command pattern for such cases. It adds more legacy to the code than what we gain: code becomes harder to read and if you have to debug it you may get crazy if the command level is more than one. Lambda does the same but hide the complexity some how and we can read it easier.
For a comparison with pros and limitations of this solution, please check the chapter Usage summary for a full picture. Now let's see if the class solution can solve these challenges or not.
Class solution
Introduction
First, the spec is unchanged. We keep the same requirements as in the Scenario. The objective of this solution is to solve the issues in the "should be avoided" cases in method level above. The solution I mentioned below bases on the support from Spring. Since at ELCA we use Spring as our main framework in work I use it to demonstrate my idea. (I don't know the other frameworks can have something similar or not). Thanks to Spring the combination between command pattern and factory pattern can bring a super easy solution for us.
Implementation
The idea here is that I will have a list of parser and each parser support two main methods: "boolean canParse(String testCommand);" and "String executeCommand(String command);". What the main "executeCommand" method does is to loop through the parsers list to see if there is any parser support the input command or not. The first one found will be used to parse the command. Tada, you can see know that the logic inside the "executeCommand" method will become easy and unchanged (even if you change the logic of welcome message). You will have something like this
Here I take advantage of the bean discovery mechanism of Spring to automatically detect the parser beans. If your project doesn't use Spring you can find the supported IoC from the framework being used or you can implement yourself. This IoC mechanism is the key point for this solution.
Sample solution with using Spring bean autowire mechanism - ClassSolutionImpl.java Expand source
And here is a class diagram of the parser
![class diagram of the parser](/sites/default/files/2024-05/Java2.png)
Explanation
- CommandParser is the interface for all the classes
- We have three implementations corresponding to three supported kinds of animal
- The Abstract class AbstractCommandParser is used to make sure all the parsers go in the same structure.
For the detail of the classes you can check in the attached source code below. Here are some useful code snippet so that you can quickly understand the solution (The Animal and AnimalType is reused here)
CommandParser.java Expand source
AbstractCommandParser.java Expand source
CatCommandParserImpl.java Expand source
Summary
As you can see this approach solves the limitations of the method level: Each animal type is in a separated class
- We can implement multiple animal types in parallel with less risk of git conflict
- We can handle complex logic for any animal types while keeping the main logic clean and clear.
- Change for an animal type is minimized
Challenges
- More classes to load. Well, if you have hundreds of animals => hundreds of classes are loaded => slower startup time and since the list of animal types is longer => it takes longer time to jump to the expected animal type parser.
- If the business logic of the canParse() is complex and take long to process => we have to wait a very long time for the turn of the expected animal type parser to do.
- Every time we add or remove animal type we need to rebuild the whole application => availability of the application is not ensured (Of-course you can use load balancer or multiple nodes solution to solve but if you have only one application instance it's a problem).
- If the animal type parser is a service which requires high availability/performance we can't do that (because there is no way to do load balancing, multiple pods for it). This need can become critical in case we have a specific animal type which need to be available 24/7 and be as fast as possible.
So, let's see if Module level solution can help you with these limitations.
Module solution
Introduction
First, the spec is unchanged. We keep the same requirements as in the Scenario. With this solution we move the animal type parser classes into JAR modules and add dependencies from the current module to those JAR modules. That's the implementation idea of Module level. So, there is no implementation demo here because the different between this and the class level is only modularization of the application 😎.
Summary
This solution includes all the Pros from class level because they are only different from code organization. Beside, it also has some more advantages
- We don't have to rebuild the main module (the one with the main executeCommand() method). So, if you have a problem with build time of the main module you can be better with this solution => The plugins ability is higher.
- Class loading is improved here because we can control which animal type to include in the loading.
- Adding or removing the animal type parser doesn't require the application to be built. Now with a restart or even hot reload you can have a new parser ready.
However, this solution still can't solve the challenges of class level. (For availability, yeah, we have something like hot reload but it's not for production env, only test env can take advantage of this. A restart of the application instance still takes times no matter how fast it is). So, let's see if microservice level can solves the remained challenges of Class and Module solutions.
Microservices solution
Introduction
Due to the limitation of the source code (I don't want to let you setup docker-compose for a whole microservices system because it will raises tons of configuration questions) I will change the spec a bit. Now there is no more executeCommand() method to implement. There is just one main() method and with the inputs as defined in the Scenario you need to output the same as you did with class solution and method solution.
Now, about microservice level. The objective of this solution is to solve the two hot points which class solution and module solution can't
- High availability of the main module as well as animal type parser
- Remove the unnecessary waiting time of the canParse() by detect and jump directly to the expected animal type parser
Implementation
The picture below shows how microservices solution is implemented for this scenari
![microservices solution](/sites/default/files/2024-05/Java3.png)
- Welcome application will send a message #1 to queue with a dedicated destination
- Base on the destination the corresponding consumer (Cat service, Dog service, Mouse service) will receive and build welcome message #2
- Welcome message is sent to the queue with destination to welcome application
- Welcome application receives and display the welcome message.
The main entry point I suggest you to go with the source code for debugging is "MicroserviceSolutionDemoApplication".
Click here to view how to run/debug the application in the source code chapter...
Summary
- High availability of the animal type parser is ensured (we can apply load balancing, multiple nodes deployment on a specific nodes if necessary): Adding/removing a new microservices can affect at runtime and doesn't affect the running system.
- Detection of the parser is done effectively => reduce many unnecessary load and processes for the system
- Have higher control as well as configurable possibility on a specific animal type parser (add more node for parser, improve speed of message queue, add power to the parser node)=> we can maximize our resource usage for business value.
- Enabling the easiness to add parallel support for the parser which is very difficult and legacy for the previous solution (we can but it requires a lot of coding)
Challenges
- The approach changes from synchronous to asynchronous. It makes the application more difficult to implement and handle exception
- Be careful when we use the synchronous approach of the jmsTemplate. Actually it's asynchronous with waiting loop to wait for the coming back result in a defined timeout.
- If we don't have the freedom in defining the topic/destination then we have to define the message selector in this case and it makes the @JmsListener become tricky to developer. Refer to this and this for information how to do with message selector
- The performance of this solution depends on not only the application but also the network. So, sometimes it's become tricky to us because network is something we can't control.
Cloud solution
Implementation
![Cloud solution Implementation](/sites/default/files/2024-05/Java6.png)
The picture above shows how the solution will be implemented: we will have 4 components:
- REST CLIENT: is the one which sends the welcome command to process
- Happy farm service: receive the welcome command then call the corresponding services (cat/dog/mouse) to parse and process the welcome command, then returns the answer to the client.
- Cat/Dog/Mouse service: parses the command sent from Happy farm service and return the welcome message.
- Eureka server: receives the registration from the cloud service, resolve the corresponding URL from the service-id in the rest template.
In the source code chapter below you can find the demo application. Below are the steps to run it.
Click here to expand...
Summary
As you can see. now because it's REST service and you can add load balancing, running in multiple nodes for a single service to make sure that it can perform with most productivity as you want. Things seems to be good? 😜 There is no really good thing. Same in this case. Here the points which affect your application will be
- Infra setup: yes, a cloud service infra is not simple. If you are using it without configuring correctly your application may run but you cost more than you should. Another point is that without monitoring and clear view of the system architect you may get lost in the services → management is a challenge here
- Other stuff around the application: availability is only a perspective. There are also other things: authentication, integrity, audit. With cloud doing these stuffs is a challenge. Your application may work at a point of time it can get the customer in but what keeps the user with us is the stability and integrity.
- Slow network: with cloud your network is really a critical points. For critical business path we may need to have backup solutions so that the application can still work or recover in case network is down.
Usage summary
From the pros and challenges of the solutions above I summarize to the table below. I give you a way to think because it's mostly my personal opinion, please use it in a flexible way
![pros and challenges of the solutions](/sites/default/files/2024-05/Readmore.jpg)
Final words
These are the ways I know and applied in the project. There may be more ways (techniques is changing day by day ). As usual, don't limit your thought, study new supports from the languages and frameworks you can make your code better and your life easier
Source code
Please find in the attached file HappyFarm-Plugins.zip for the source code. This source code is just for demonstration purpose. The skeleton and setup inside should be used for reference only.
- For Method and Class: there are some unit tests. You can run them to see the result
- For Microservice: only main() method to run. Since I use the embedded ActiveMQ in spring boot after you terminate the application you may need to go to the Task windows and kill JAVA processes if it's still there.
- For the Cloud demo, when you start the client services (Cat, happy farm,...) you may see some exception. It's because I don't setup the full cloud infra, you just ignore it.
- The source code is from maven but if you run maven from the outside you may get issues because I use GUI of intellij to add maven modules and I don't intend to fix it now. Just follow the ways in step 1-3 above to check the demo.