Skip to main content
Create your own
Lesson illustration

Adapter Pattern: Bridging Incompatible Interfaces

Hello! Welcome to the first lesson in our module on Structural Design Patterns.

In the previous module, we focused on Creational Patterns, which provide various mechanisms for object creation. We concluded by comparing them to understand when to use each one. Now, we shift our focus from creating objects to structuring them. Structural patterns are all about how classes and objects can be composed to form larger, more flexible structures.

Today, we'll start with one of the most practical and widely used structural patterns. Your learning outcome is to apply the Adapter pattern to make incompatible interfaces work together.

Imagine you have a laptop with a US-style plug, but you're in a country with European-style wall sockets. The two are incompatible. What do you do? You use a travel plug adapter. It doesn't change your laptop or the wall socket; it simply acts as a bridge, translating one interface into another. The Adapter design pattern does the exact same thing for objects in your code.

When Do We Need an Adapter?

In software development, you'll frequently encounter situations where you need to integrate a new component, a third-party library, or a legacy system whose interface doesn't match what your existing client code expects. Rewriting your client code or the component you're integrating is often not feasible or desirable. This is the core problem the Adapter pattern solves.

Adapter Pattern: Real Life Problems, Java Code

To see how common this problem is, let's explore some real-world scenarios where the Adapter pattern is a perfect fit. This will help you build an intuition for identifying when to use it.

Please read the section titled 'Real-World Problem Statements That Justify the Adapter Pattern'. Pay attention to the variety of examples, from legacy banking systems to modern smart home hubs.

As you can see, the problem of incompatible interfaces is everywhere. The Adapter pattern provides an elegant solution by introducing a new class that "wraps" the incompatible object and exposes the interface that the client expects.

The Components of the Adapter Pattern

The pattern has three key participants:

  1. Target Interface: This is the interface your client code is designed to work with. It's the "socket" in our analogy.
  2. Adaptee: This is the existing class with an incompatible interface that you want to reuse. It's the "plug" you have.
  3. Adapter: This is the class that bridges the gap. It implements the Target interface and internally holds a reference to an Adaptee object. When a client calls a method on the adapter, the adapter translates that call into one or more calls on the wrapped adaptee.

This diagram shows how these components relate to each other.

Class Diagram of Adapter Design Pattern
This UML diagram illustrates the relationship between the Client, the Target interface ('Printer'), the Adaptee ('LegacyPrinter'), and the Adapter ('PrinterAdapter'). The client interacts with the Target, and the Adapter makes the Adaptee conform to that Target interface.

A Practical Example: Integrating Payment Gateways

Let's dive into a concrete example that's highly relevant in modern application development, especially with your background in Java Spring Boot. Imagine your e-commerce application needs to support multiple payment gateways like PayPal, Stripe, and Razorpay. Each of these will have its own SDK with unique method names and request/response formats.

Your application's business logic shouldn't have to contain if/else statements for every payment gateway. That would be a nightmare to maintain and extend. We need a unified interface.

Adapter Design Pattern Explained with Spring Boot | Real-Time Example | @Javatechie

The following video by 'Java Techie' provides an excellent, real-time demonstration of implementing the Adapter pattern in a Spring Boot application to solve this exact problem. You'll see the code evolve from a tightly-coupled mess to a clean, extensible design.

Please watch the video from the beginning until 09:00. (00:00 - 02:59): Focus on the real-world analogy (phone charger) and the formal definition of the pattern. (02:59 - 05:45): Understand the payment gateway problem and how a tightly coupled solution becomes problematic. (05:45 - 09:00): Observe the initial, non-adapter implementation. Notice how adding a new gateway (GPay) requires modifying the main service class. This demonstrates the problem we want to solve.

The initial code works, but it violates the Open/Closed Principle. To add a new payment gateway, you have to modify the PaymentService class. Now, let's see how we can refactor this using interfaces and, eventually, the Adapter pattern to create a truly plug-and-play system.

Adapter Design Pattern Explained with Spring Boot | Real-Time Example | @Javatechie

Now, let's watch the rest of the video to see the solution unfold. The author first introduces an intermediate refactoring before moving to the full adapter implementation.

Please watch the video from 09:00 to the end. (09:00 - 16:15): The first refactoring introduces a common PaymentProcessor interface and a factory method. This is an improvement, but it still requires modifying the factory method to add new gateways. (16:15 - 23:30): This is the core of the Adapter pattern implementation. The services are renamed to Adapters. Notice the use of a Map to dynamically register and retrieve the correct adapter. This eliminates the if/else or switch logic entirely. (23:30 - End): The final part demonstrates the power of this pattern. A new PhonePeAdapter is added with zero changes to the existing service logic. This is the key benefit.

This video brilliantly demonstrates a modern, practical implementation. Here, our components are:

  • Target: The PaymentProcessor interface. This is the unified interface our PaymentService works with.
  • Adaptees: The actual third-party payment gateway SDKs (which are simulated here by simple System.out.println calls).
  • Adapters: The PayPalAdapter, StripeAdapter, GooglePayAdapter, etc. Each of these classes implements PaymentProcessor and translates the call to the specific methods of its corresponding adaptee.

The use of Spring's Dependency Injection and a Map to create a registry of adapters is a powerful technique that makes the system extremely flexible.

Test your understanding!

You are working on a data processing pipeline. Your application works with a DataAnalytics interface that has a method processJson(String jsonData).

You need to integrate a legacy library from another team. This library has a class called OldAnalyticsTool with a method analyzeXml(String xmlData). You cannot change the legacy library.

How would you use the Adapter pattern to make the OldAnalyticsTool work with your application? Describe the classes and interfaces you would create.

Show answer
  1. Target Interface: DataAnalytics (This already exists).

  2. Adaptee: OldAnalyticsTool (The legacy class).

  3. Adapter Class: You would create a new class, let's call it XmlAnalyticsAdapter.

    • This XmlAnalyticsAdapter class will implement the DataAnalytics interface.
    • It will hold a private instance of the OldAnalyticsTool (using composition).
    • It will implement the processJson(String jsonData) method required by the DataAnalytics interface.
    • Inside the processJson method, it will perform the "translation":
      1. Convert the incoming jsonData string into an XML format string.
      2. Call the analyzeXml() method on its internal OldAnalyticsTool instance, passing the converted XML data.
    // 1. Target Interface (what our client uses)
    public interface DataAnalytics {
        void processJson(String jsonData);
    }
    
    // 2. Adaptee (the legacy/incompatible class)
    public class OldAnalyticsTool {
        public void analyzeXml(String xmlData) {
            System.out.println("Legacy tool is analyzing XML data: " + xmlData);
        }
    }
    
    // 3. Adapter (bridges the gap)
    public class XmlAnalyticsAdapter implements DataAnalytics {
        private OldAnalyticsTool oldTool;
    
        public XmlAnalyticsAdapter(OldAnalyticsTool oldTool) {
            this.oldTool = oldTool;
        }
    
        @Override
        public void processJson(String jsonData) {
            // Step 1: Translate the interface/data
            String xmlData = convertJsonToXml(jsonData); // Assume this method exists
            
            // Step 2: Delegate the call to the adaptee
            oldTool.analyzeXml(xmlData);
        }
    
        private String convertJsonToXml(String json) {
            // In a real scenario, this would involve a library like Jackson or manual conversion.
            System.out.println("Adapter is converting JSON to XML...");
            return "<data>" + json + "</data>";
        }
    }
    

Object Adapter vs. Class Adapter

The implementation we've been looking at is called an Object Adapter because it uses composition—the adapter holds an instance of the adaptee. There is another type called a Class Adapter, which uses inheritance.

  • Object Adapter (Composition): The adapter implements the target interface and wraps an object of the adaptee class. This is the most common and flexible approach in Java.
  • Class Adapter (Inheritance): The adapter extends the adaptee class and implements the target interface. This requires multiple inheritance (inheriting from both a class and an interface), which Java does not support for classes. Therefore, a true Class Adapter is not possible in Java if the adaptee is a class.

Adapter Design Pattern in Java - Tutorial

The following article from vogella.com explains this distinction clearly.

Please read the section 'Types of Adapters', which covers both 'Object Adapter' and 'Class Adapter'. Focus on why the Object Adapter approach is preferred in Java.

Because you should favor composition over inheritance (a core design principle we'll cover later), and due to Java's language constraints, you will almost always use the Object Adapter pattern.

Conclusion

The Adapter pattern is a simple yet powerful tool for dealing with the inevitable reality of incompatible interfaces. It allows you to integrate new components, third-party libraries, and legacy systems cleanly, without modifying existing code.

Key Takeaways:

  • Purpose: To allow objects with incompatible interfaces to collaborate.
  • Structure: It involves a Target interface (what the client expects), an Adaptee (what you have), and an Adapter (the bridge).
  • Implementation: The most common approach in Java is the Object Adapter, which uses composition to wrap the adaptee.
  • Benefit: It promotes code reuse and follows the Open/Closed Principle by allowing you to add new adapters without modifying client code.

In our next lesson, we'll explore another structural pattern: the Decorator. While an Adapter changes an object's interface, a Decorator adds new responsibilities to an object dynamically, without changing its interface. Understanding the distinction between these two will be a key focus.

Can't find a good explanation? Sign up and we'll make it for you

Sign up