Implement Strategy Pattern with Spring Boot

Advocate for better developer's productivity and experience
Documenting my learnings and sharing it
Search for a command to run...

Advocate for better developer's productivity and experience
Documenting my learnings and sharing it
Background Recently, I redesigned my team's pipeline running on a multi-module Maven monorepo using GitLab CI. It wasn't that the previous setup was broken, but my team faced a few persistent issues t
Background At work, there’s some test written where I wanted to query the database (repository) after mockMvc request has been made to verify the result. This is a very simple illustration of what it looks like. @SpringBootTest @AutoConfigureMockMvc ...

It's been a long time coming, but I finally came around to set up MongoDB with replicas for local development. TLDR; clone the repository, run docker compose up -d, and you are good to go! If you want to know more, continue to read below. Context Run...

Context I recently came across TaskFile and wanted to use it to automate some of my workflows, which underlying invokes PowerShell commands. However, I faced several errors while doing so, and this post is to reflect on it, and hopefully help myself ...

Problem I was always out of disk space in my C drive, and even though I clear my stuff often, I notice that my space is never reclaimed, instead, it kept going down. I used WizTree to view which file is taking up my disk space, and I noticed this No...

Strategy pattern is a behavior design pattern which allows us to select the strategy at runtime. This is particularly useful in the context where the application determines the strategy to run based on user input/selection.
Traditionally, in a strategy pattern implementation, we would have to use a Context which acts as a middleman between the client and the strategy executor. However, in the case of Spring, the implementation is much simpler as there is no need for a Context in order for this to work, which makes the implementation much more elegant.
In this demo, we will write a less efficient implementation first, and then attempt to enhance it.
Imagine that we have an API that provides flight information.
@RestController
public class FlightController {
@GetMapping("/flights/{airline}")
public String getFlightInfo(@PathVariable("airline") String airline) {
// omitted
}
}
And an interface
public interface FlightInfo {
String display();
}
Where different airline company would implement this interface, like such
@Service
public class SingaporeAir implements FlightInfo {
@Override
public String display() {
return "Singapore Airlines";
}
}
Now, let's go back to our controller and finish up the implementation
@RestController
@RequiredArgsConstructor
public class FlightController {
private final SingaporeAir singaporeAir; // 1
private final ThaiAir thaiAir; // 1
@GetMapping("/flights/{airline}")
public String getFlightInfo(@PathVariable("airline") String airline) {
if ("ThaiAir".equals(airline)) { // 2
return this.thaiAir.display(); // 3
}
if ("SingaporeAir".equals(airline)) { // 2
return this.singaporeAir.display(); // 3
}
return "N.A";
}
}
You probably will end up with something like the above, where
FlightInfo interfaceLet's try to hit against the API, and see the output

Excellent, we have done our job, and let's call it a day. But, on the next day, we need to add support to provide more airline information. With the current implementation, it's not going to be maintainable in the long run, where we would either end up with lots of if statements, or switch case statement. And that is not ideal where with each new implementation, we would need to make changes to the original code.
This is where we can enhance our current implementation.
Firstly, let's make some tweaks to some existing classes.
@Service("SingaporeAir")
public class SingaporeAir implements FlightInfo {
// omitted, no change from before
}
@Service("ThaiAir")
public class ThaiAir implements FlightInfo {
// omitted, no change from before
}
We added the beanName so to easily match our bean against the input later on.
@RestController
@RequiredArgsConstructor // 1
public class FlightController {
private final Map<String, FlightInfo> flightInfoMap; //2
@GetMapping("/flights/{airline}")
public String getFlightInfo(@PathVariable("airline") String airline) {
FlightInfo flightInfo = this.flightInfoMap.get(airline); // 3
if (flightInfo != null) {
return flightInfo.display(); // 4
}
return "N.A";
}
}
Spring allows us to inject via a List or Map. In this case, we are using Map and the key would be the beanName which we declared previouslybeanName, and thus, extract the correct service from the Mapdisplay method to grab the valueNotice that now, we are only injecting a Map of FlightInfo instead of injecting the individual service(s). This way, when we introduce more airlines that implements FlightInfo, there would be no change to the existing code.
@Service("Emirates")
public class Emirates implements FlightInfo {
// omitted
}

How awesome is that?
We looked at how we implemented Strategy Pattern with Spring Boot using a less maintainable way, and then enhanced the implementation by injecting a Map which allow us to look up the respective implementation and trigger the correct method call.
As usual, full source code is available in GitHub