Using Interface as Dependency in Spring Boot Application

This is one of the frequently asked questions to me and in the post, I am going explain why the Spring team recommends using interfaces as dependencies in Spring or Spring boot applications.

The Spring team recommends using interfaces as dependencies to promote loose coupling and increase flexibility in your codebase. This practice is commonly known as "programming to interfaces."

By depending on interfaces rather than concrete implementations, you decouple your components from specific implementations, allowing for easier swapping of implementations or introducing new implementations in the future. This promotes modularity and makes your code more maintainable and extensible.

Spring's dependency injection mechanism allows you to inject dependencies into your components at runtime. When using interfaces, you can declare dependencies as interfaces, and Spring can dynamically provide the appropriate implementation based on the configuration or annotations. This enables loose coupling between components and promotes flexibility and testability.

Let me Explain with an Example

Suppose you have an application that requires sending notifications to users through various channels like email, SMS, and push notifications. You want your application to be flexible, allowing you to easily switch between different notification implementations or add new ones in the future.

To achieve this, you can define an interface called NotificationService that provides a contract for sending notifications:
public interface NotificationService {
    void sendNotification(String message, String recipient);
}
Next, you can create different implementations of the NotificationService interface for each channel. For example, let's create an EmailNotificationService and a SMSNotificationService:

public class EmailNotificationService implements NotificationService {
    public void sendNotification(String message, String recipient) {
        // Logic to send an email notification
    }
}

public class SMSNotificationService implements NotificationService {
    public void sendNotification(String message, String recipient) {
        // Logic to send an SMS notification
    }
}
Now, let's say you have a class called NotificationSender that needs to send notifications. Instead of directly depending on concrete implementations like EmailNotificationService or SMSNotificationService, you can depend on the NotificationService interface:

public class NotificationSender {
    private final NotificationService notificationService;

    public NotificationSender(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void sendNotification(String message, String recipient) {
        notificationService.sendNotification(message, recipient);
    }
}
By depending on the NotificationService interface, the NotificationSender class doesn't need to know which specific implementation it's using. It only requires an object that adheres to the NotificationService contract. This allows for greater flexibility and extensibility. Now, using Spring's dependency injection, you can configure the appropriate implementation to be injected into the NotificationSender class based on your needs. 

For example:
@Configuration
public class AppConfig {
    @Bean
    public NotificationService notificationService() {
        // return the desired implementation, e.g., EmailNotificationService or SMSNotificationService
    }

    @Bean
    public NotificationSender notificationSender(NotificationService notificationService) {
        return new NotificationSender(notificationService);
    }
}
With this configuration, you can easily switch between different notification implementations by changing the notificationService() method in the configuration class without modifying the NotificationSender class. This promotes loose coupling and makes your code more maintainable and flexible.

Additionally, when writing unit tests, you can easily create mock implementations of the NotificationService interface to isolate and test the NotificationSender class without relying on real email or SMS services.

Conclusion

By depending on interfaces rather than concrete implementations, you achieve loose coupling, flexibility, and better testability in your codebase, allowing you to easily adapt to changes and extend your application's functionality in the future.

Comments