Welcome back! In our last lesson, we explored how interfaces allow us to define capabilities, like Loggable and Auditable, that can be mixed into any class. You saw how EmailService could be Loggable while SMSService could be both Loggable and Auditable, demonstrating a powerful way to add behaviors without being constrained by single class inheritance.
We ended with a preview of a core design principle that gives interfaces their true power. This lesson is dedicated to that principle: programming to an interface, not a concrete implementation. This concept is the foundation of flexible, maintainable, and scalable software—precisely the kind you'll build in backend development with frameworks like Spring. We will explore what "coupling" means, why tight coupling is a problem, and how interfaces provide the solution through loose coupling.
The Rigidity of Tight Coupling
In software design, coupling refers to the degree of interdependence between different modules or classes. Imagine two gears in a machine. If they are tightly meshed, you can't change one without re-engineering the other. This is tight coupling. If they are connected by a flexible belt, you can swap out one gear for a different size without affecting the other. This is loose coupling.
In code, tight coupling happens when one class directly depends on the concrete implementation of another class. This makes your system rigid. If you need to change or replace that implementation, you are forced to make changes in the dependent class as well.
To get a clear, narrative understanding of this problem, watch the following video from Telusko. It tells a simple story about a developer who needs a computer, perfectly illustrating why tying your code to a specific "Laptop" class is a bad idea.
This video builds a scenario that clearly demonstrates the problem of tight coupling and how abstraction—first with an abstract class and then with an interface—solves it.
First, watch the initial problem setup from the beginning. Notice how the Developer class is hardcoded to work only with a Laptop object. The problem becomes obvious when a Desktop is introduced. Next, observe the solution using an abstract class from this section. The Developer now depends on an abstract Computer, making the code more flexible. This should feel familiar from our lesson on abstract classes. Finally, see how an interface can achieve the same goal, often more cleanly, in the last part of the video from this segment. The key takeaway is that the Developer class depends on a contract (Computer), not a specific machine.
Achieving Flexibility with Loose Coupling
As the video showed, the solution to tight coupling is to introduce an abstraction that both classes can agree on. This abstraction is the contract, and in Java, interfaces are the perfect tool for defining such contracts.
"Program to an interface, not an implementation" means that your variables, parameters, and return types should be of an interface type whenever possible, rather than a specific class type.
This diagram visually captures the idea. The "Core of program" doesn't know or care about class A or class B. It only interacts with MyInterface. This allows A or B (or a new C) to be plugged in without changing the core program at all.

The following article from Scaler Topics provides a more formal definition and some excellent code examples.
Coupling in Java with Examples- Scaler Topics
This article breaks down the concepts of tight and loose coupling with clear definitions and practical code.
Start by reading the introduction, "What is Coupling in Java?", and the definitions for Loose Coupling and Tight Coupling. This will solidify your understanding of the terminology. Next, study Example 1: Online Shopping Application. Pay close attention to the "Without Loose Coupling" and "With Loose Coupling" code snippets. Notice how the Cart class changes from depending on a concrete Payment class to a PaymentProcessor interface. Also review Example 2: Messaging System, which reinforces the same pattern with a Notifier interface. Finally, read the section comparing loose vs. tight coupling and the conclusion under "Which is Better?". The mention of Spring's Dependency Injection is a direct link to your goal of enterprise development—this principle is why Spring works the way it does.
The Three Rules of Programming to an Interface
So, how do we apply this principle in our day-to-day coding? The "Java Guides" channel provides a concise video that boils it down to three practical rules.
Java Best Practices | Code for Interface Not for Implementation | PART-1
This video is a focused tutorial on the best practice of "coding for an interface," which is exactly our topic. It provides simple, memorable rules and examples.
Watch the introduction from the start to understand the importance of this best practice. Pay close attention at this timestamp, where the presenter outlines the three key applications of this principle. I'll summarize them here: Use interface type as a reference type. Use interface type as a return type. Use interface type as a method argument. Watch the practical demonstration using a Shape interface (Shape example) and the Java Collections library (Collections example). This shows how you can easily swap implementations (Circle for Rectangle, ArrayList for LinkedList) when your code depends on the interface (Shape, List).
Let's recap those three rules:
- Reference Types: Instead of
ArrayList<String> names = new ArrayList<>();, preferList<String> names = new ArrayList<>();. This allows you to change the implementation toLinkedListlater with minimal code changes. - Method Arguments: A method signature like
public void processData(ArrayList<String> data)is brittle. A much better signature ispublic void processData(List<String> data). Now the method can accept any kind ofList, not just anArrayList. - Return Types: A method that returns
ArrayList<String>forces its callers to work with anArrayList. If the method instead returnsList<String>, callers are free to treat it as a genericList, and you are free to change the method's internal implementation to return aLinkedListin the future without breaking any calling code.
Your Turn: Refactoring for Loose Coupling
Now, let's apply this powerful principle to our notification system. In the previous lessons, we had an abstract NotificationService class. We're going to refactor this to use an interface, and in doing so, we'll create a new NotificationManager class that is completely decoupled from the specific notification methods. This is a foundational step toward the Dependency Injection pattern you'll see everywhere in Spring.
Task:
-
Create the
NotificationServiceInterface:- Instead of an abstract class, create a new interface named
NotificationService. - Give it a single abstract method:
void send(String message, String recipient);.
- Instead of an abstract class, create a new interface named
-
Refactor
EmailServiceandSMSService:- Modify both
EmailServiceandSMSServiceclasses. - Remove
extends NotificationService(the old abstract class). - Change it to
implements NotificationService(the new interface). You can delete the old abstract class file. - Your classes will now implement multiple interfaces, for example:
public class SMSService implements NotificationService, Loggable, Auditable. This is perfectly fine and very common. - Ensure both classes provide a concrete implementation for the
sendmethod from the new interface.
- Modify both
-
Create a Decoupled
NotificationManager:- Create a new class named
NotificationManager. This class's only job is to manage the sending of a notification, without knowing how it's sent. - Inside
NotificationManager, add a private field of the interface type:private NotificationService notificationService; - Create a constructor that accepts a
NotificationServiceobject and assigns it to this field. This technique is called constructor injection.public NotificationManager(NotificationService service) { this.notificationService = service; } - Create a public method that uses the service:
public void doNotify(String message, String recipient) { // The manager only knows about the 'send' contract. // It has no idea if it's an email, SMS, or something else. this.notificationService.send(message, recipient); }
- Create a new class named
-
Test in
Main:- In your
mainmethod, wire everything together and see the flexibility in action. - Instantiate your concrete services:
(Notice we are already using the interface as the reference type!)NotificationService email = new EmailService(); NotificationService sms = new SMSService(); - Create a
NotificationManagerconfigured for email and use it:System.out.println("--- Testing with EmailService ---"); NotificationManager emailManager = new NotificationManager(email); emailManager.doNotify("Your order has been shipped.", "customer@example.com"); - Now, create another
NotificationManagerconfigured for SMS and use it, without changing the manager's code at all:System.out.println("\n--- Testing with SMSService ---"); NotificationManager smsManager = new NotificationManager(sms); smsManager.doNotify("Your package is out for delivery.", "+1234567890");
- In your
By completing this exercise, you will have successfully demonstrated loose coupling. Your NotificationManager class is completely independent of EmailService and SMSService. It only depends on the NotificationService contract, making your system flexible and easy to extend with new notification types in the future (e.g., PushNotificationService).
Conclusion
Today, you've moved from understanding Java syntax to applying a crucial software design principle. Loose coupling is not just an academic concept; it is a practical necessity for building the kind of large-scale, resilient backend applications you aim to master. By programming to interfaces, you make your code more modular, more testable, and far easier to maintain and extend over time.
Key Takeaways:
- Tight Coupling makes systems rigid and hard to change because components are highly interdependent.
- Loose Coupling makes systems flexible and modular by minimizing dependencies between components.
- The principle "Program to an interface, not an implementation" is the primary way to achieve loose coupling in Java.
- This is applied by using interface types for reference variables, method arguments, and method return types.
- This approach, especially when combined with injecting dependencies (like you did in
NotificationManager), is a cornerstone of modern frameworks like Spring.
In our next lesson, we'll look at enums. While interfaces give us flexibility in behavior, enums provide a type-safe way to represent a fixed set of constants, another important tool for writing clean and robust code.