Let’s implement Strategy Pattern in Spring Boot with Java in the most efficient manner.
Hey guys, welcome back to my YouTube channel (because I didn’t know how to start this one).
I don’t intend to create another redundant article that explains how to implement design patterns. This one actually focuses on implementing the strategy pattern in Spring Boot and Java using the power of Spring Dependency Resolver.
In most cases, engineers end up using multiple if/else conditions. Or a switch case in a “cleaner” implementation of switching between the relevant strategies.
Spring boot resolves dependencies like no one else does. It enables you to implement the strategy pattern in a scalable and efficient manner.
We will discuss an approach where we can create implementation of a strategy interface and Spring Boot will take care of the rest. How? By picking up the concrete types and mapping each type to its concrete implementation.
How to implement Strategy Pattern in Spring Boot
Let’s consider a situation where we have to create a notification service. The service is responsible to send notifications to the user, based on the provided type. This means, if the provided type is email
, it should send an email to the user. And a push notification
or an SMS
respectively. These are three different strategies with each having its own business logic to execute.
Lets start with defining class for the supported notification types.
NotificationType.java
/**
* @author thegeekyasian
*/
public class NotificationType {
public static final String EMAIL = "email";
public static final String SMS = "sms";
public static final String PUSH_NOTIFICATION = "push_notification";
}
The idea behind creating NotificationType is to define supported values, and have them defined in a single place.
The next step is to create an interface
for our service, called Notification Service.
NotificationService.java
/**
* The NotificationService interface defines the contract
* for strategies that provide notification services.
* Notification services can send notifications
* and provide the type of notifications that they can send.
*
* @author thegeekyasian
*/
public interface NotificationService {
/**
* Sends a notification.
* It can be customized and have parameters as input,
* can also be modified to have a return type.
*/
void sendNotification();
}
Now that we have created an interface, let’s provide an implementation for each of our notification types i.e. strategies.
EmailNotificationService.java
/**
* @author thegeekyasian
*/
@Service(NotificationType.EMAIL)
public class EmailNotificationService implements NotificationService {
@Override
public void sendNotification() {
System.out.println("Sending email");
}
}
PushNotificationService.java
@Service(NotificationType.PUSH_NOTIFICATION)
public class PushNotificationService implements NotificationService {
@Override
public void sendNotification() {
System.out.println("Sending push notification");
}
}
SmsNotificationService.java
/**
* @author thegeekyasian
*/
@Service(NotificationType.SMS)
public class SmsNotificationService implements NotificationService {
@Override
public void sendNotification() {
System.out.println("Sending SMS");
}
}
We have our strategies in place now, as we have created a separate implementation for each execution strategy.
What’s next? We need to create a Factory that maps the value of the notification type to each implementation. For that, we will leverage Spring Boot’s dependency injection to resolve the interface to each of the three concrete types.
Since each strategy is marked as a Service with @Service
annotation, we can get all the concrete implementations of NotificationService
interface by Autowiring NotificationService
.
This will pick all the implementations of NotificationService
in a Map
, by setting each implementation against the provided Bean name in the @Service
annotation.
Sounds a lot? Let’s see how simple it is. Here is what our NotificationFactory class looks like:
NotificationFactory.java
/**
* A factory class for creating instances of
* NotificationService based on a provided notification type.
*
* @author thegeekyasian
*/
@Component
public class NotificationFactory {
/**
* A map that contains NotificationService instances
* mapped to their corresponding notification types.
*/
private final Map<String, NotificationService> notificationServiceMap;
/**
* Uses constructor-autowiring to create a new NotificationFactory instance
* with the provided map of NotificationService instances.
*
* @param notificationServices a map of NotificationService instances
*/
public NotificationFactory(Map<String, NotificationService> notificationServices) {
this.notificationServiceMap = notificationServices;
}
/**
* Returns the NotificationService instance
* corresponding to the provided notification type.
*
* @param notificationType the type of notification
* @return the NotificationService instance corresponding to the provided notification type
* @throws RuntimeException if the provided notification type is not supported
*/
public NotificationService getNotificationService(String notificationType) {
NotificationService notificationService = notificationServiceMap.get(notificationType);
if (notificationService == null) {
throw new RuntimeException("Unsupported notification type");
}
return notificationService;
}
/**
* Executes the sendNotification() method on the NotificationService
* instance corresponding to the provided notification type.
* @param notificationType the type of notification to execute
* @throws RuntimeException if the provided notification type is not supported
*/
public void execute(String notificationType) {
NotificationService notificationService = getNotificationService(notificationType);
notificationService.sendNotification();
}
}
How do we test it? Since NotificationFactory
is marked as a @Component
, it also registered as a Spring bean.
We can create a new instance of the NotificationFactory
by Autowiring it as well.
Lets take an example of NotificationExecutor
, which calls notification service for each of these notification types
NotificationExecutor.java
/**
* @author thegeekyasian
*/
@Service
public class NotificationExecutor {
private final NotificationFactory notificationFactory;
public NotificationExecutor(NotificationFactory notificationFactory) {
this.notificationFactory = notificationFactory;
}
@PostConstruct
public void test() {
notificationFactory.execute(NotificationType.EMAIL); // prints `Sending email`
notificationFactory.execute(NotificationType.PUSH_NOTIFICATION); // prints `Sending push notification`
notificationFactory.execute(NotificationType.SMS); // prints `Sending SMS`
}
}
What’s next? Nothing. Yaas, that’s how simple it was.
Now, if you have to provide a new implementation for another notification type (maybe Phone Call), all you have to do is just provide an implementation of PhoneCallNotificationService
, and let Spring do the magic.
I have published a working version of this project on my GitHub repository.
If you found it helpful, do not hesitate to add some stars on the project repository.
For any questions, support or feedback, feel free to join my Discord Community.
Leave a Reply