Published on

Factory Method Design Pattern in Spring Boot

Authors

The Factory Method pattern is a creational design pattern that defines an interface for creating objects, with the specific class being determined by runtime conditions.

Essentially, it allows a class to delegate the responsibility of object instantiation to a specialized Factory class, that decides which concrete implementation to create based on runtime conditions or user input. This approach promotes loose coupling, enabling our code to be more extendable and maintainable.

In this tutorial, we'll explore the Factory Method pattern and its implementation in a Spring Boot application.

Table of Contents

Use Cases

The Factory Method pattern is useful when:

  • The type of object to create depends on runtime conditions or user input - This provides flexibility to the application, allowing it to determine the object type dynamically.
  • We want to encapsulate the instantiation logic within a factory class - By centralizing object creation in a factory, we avoid duplicating instantiation code across multiple client classes.
  • We want to adhere to the open/closed principle: The open/closed principle states that software entities should be open for extension but closed for modification. By using the Factory Method pattern, we can introduce new object types with minimal changes to the existing codebase.

Implementing the Factory Method Pattern

To demonstrate the Factory Method pattern, we’ll develop a notification service in Spring Boot that can send various types of notifications (Email, SMS, Push). The service decides the type of notification to send based on the user's preferences. This means, we need to decide the type of notification at runtime, and it may differ for each user.

Project Setup

We'll create a Spring Boot project with Maven. To get started, we need to add the following dependencies in the pom.xml file:

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

The spring-boot-starter dependency provides the core functionality of Spring Boot, while the spring-boot-starter-test dependency includes testing libraries.

The Interface

Next, we define a Notification interface that all notification types will implement. This interface will have a single method, send() that sends the notification.

public interface Notification {
    void send();
}

Implementations

We'll create two classes that implement the Notification interface: EmailNotification, and PushNotification. Each class provides a specific implementation for sending the corresponding type of notification.

The EmailNotification class implements the Notification interface and provides a specific implementation for sending email notifications. The @Service annotation registers this class as a Spring Bean, enabling Spring to manage its lifecycle.

@Service
public class EmailNotification implements Notification {
    @Override
    public void send() {
        System.out.println("Sending an Email Notification...");
    }
}

Similarly, we define the SMSNotification class as well:

@Service
public class SMSNotification implements Notification {
    @Override
    public void send() {
        System.out.println("Sending an SMS Notification...");
    }
}

The Factory Method

Next, let's create a class called NotificationFactory that will be responsible for creating instances of the appropriate notification type based on the input. This class will use Spring's dependency injection to inject the notification beans and return the correct one based on the input type.

public class NotificationFactory {
    private final EmailNotification emailNotification;
    private final SMSNotification smsNotification;

    public NotificationFactory(EmailNotification emailNotification, SMSNotification smsNotification) {
        this.emailNotification = emailNotification;
        this.smsNotification = smsNotification;
    }
    
    public Notification createNotification(String type) {
        switch (type) {
            case "email":
                return emailNotification;
            case "sms":
                return smsNotification;
            default:
                throw new IllegalArgumentException("Invalid notification type");
        }
    }
}

Let's look at the key points in the NotificationFactory class:

  • Constructor Injection: The notification beans (EmailNotification and SMSNotification) are injected into the factory class through the constructor.
  • Factory Method: The createNotification() method takes a type parameter and returns the appropriate notification bean based on the input.
  • Exception handling: If the input is invalid, it throws an IllegalArgumentException.

By using dependency injection, the NotificationFactory class is decoupled from the concrete notification classes, promoting flexibility and maintainability. At the same time, this reduces the number of objects being created as Spring manages the lifecycle of the notification beans.

Using the Factory Method

Finally, let's create a NotificationService class that uses the NotificationFactory to send notifications. The NotificationService class will have a sendNotification() method that takes the notification type as input and sends the appropriate notification.

@Service
public class NotificationService {
    private final NotificationFactory notificationFactory;

    public NotificationService(NotificationFactory notificationFactory) {
        this.notificationFactory = notificationFactory;
    }

    public void sendNotification(String type) {
        Notification notification = notificationFactory.createNotification(type);
        notification.send();
    }
}

Once again we use constructor injection to inject the NotificationFactory into the NotificationService class. The sendNotification() method takes the notification type as input, creates the appropriate notification using the factory, and sends it.

It is important to note that the NotificationService class is not aware of the concrete notification types (EmailNotification, SMSNotification). This decoupling ensures that the service class is not affected by changes in the notification types, promoting maintainability and extensibility.

Testing

Finally, let’s add unit tests to verify the behavior of NotificationService. These tests ensure that the service behaves as expected and that the correct notification type is created and sent.

@SpringBootTest
public class NotificationServiceTest {

   @Autowired
   private NotificationService notificationService;

   @MockBean
   private EmailNotification emailNotification;

   @MockBean
   private SMSNotification smsNotification;

   @Test
   public void testEmailNotification() {
      notificationService.sendNotification("email");
      Mockito.verify(emailNotification)
              .send();
   }

   // Similar tests for other notification types
}

Here are the key points in the NotificationServiceTest class:

  • @SpringBootTest: This annotation loads the full application context for integration testing. It ensures that all the beans are loaded and available for testing.
  • @MockBean: Mocks the notification beans, ensuring isolated tests without requiring actual implementations. This allows us to test the service in isolation.
  • Mockito.verify(): Verifies that the send() method is called on the correct notification type. This ensures that the correct notification type is created and sent.

Adding a New Notification Type

One of the key benefits of the Factory Method pattern is its extensibility. Let's say we want to add a new notification type, PushNotification. To do this, we need to create a new class that implements the Notification interface and provide a specific implementation for sending push notifications.

@Service
public class PushNotification implements Notification {
    @Override
    public void send() {
        System.out.println("Sending a Push Notification...");
    }
}

Next, we need to update the NotificationFactory class to include the new notification type:

public Notification createNotification(String type) {
  switch (type) {
      case "email":
          return emailNotification;
      case "sms":
          return smsNotification;
      case "push":
          return pushNotification;
      default:
          throw new IllegalArgumentException("Invalid notification type");
  }
}

As we can see, adding a new notification type is straightforward. We create a new class that implements the Notification interface, update the NotificationFactory to include the new type, and we're done. This demonstrates the flexibility and extensibility of the Factory Method pattern.

Conclusion

In this tutorial, we explored the Factory Method design pattern and its implementation in a Spring Boot application. We developed a notification service that uses the Factory Method pattern to create different types of notifications based on user preferences. Furthermore, we demonstrated how to add a new notification type to the system with minimal changes.

By delegating the responsibility of object creation to the factory class, we achieved loose coupling, maintainability, and extensibility in our codebase.

This pattern is particularly useful when the type of object to create depends on runtime conditions or user input, enabling dynamic object creation and promoting code reusability.