How to Implement Adapter Pattern for Legacy System Integration in Java

You implement the Adapter pattern for legacy system integration by creating a wrapper class that implements the modern interface your client expects while internally delegating calls to the legacy class's incompatible API, enabling seamless interoperability without modifying existing code.

When modernizing enterprise Java applications, you frequently encounter scenarios where new business logic must interact with aging third-party libraries or internal legacy systems that expose outdated APIs. Learning how to implement Adapter pattern for legacy system integration allows you to bridge these incompatible interfaces without refactoring stable, battle-tested legacy code. This article examines the canonical implementation found in the iluwatar/java-design-patterns repository, demonstrating how the pattern decouples client code from legacy implementations through object composition.

Understanding the Adapter Pattern for Legacy Integration

The Adapter pattern acts as a bridge between two incompatible interfaces, allowing objects with mismatched APIs to collaborate. In the context of legacy system integration, this means your modern application code can remain clean and interface-driven while the adapter handles the messy translation to legacy method signatures.

Core Architectural Components

The implementation in adapter/src/main/java/com/iluwatar/adapter/ defines four distinct roles:

  • Target – The interface the client expects. In the repository, RowingBoat declares void row(); and represents the modern API.
  • Adaptee – The existing legacy class with the incompatible interface. FishingBoat contains void sail(); and cannot be modified.
  • Adapter – The bridge that implements the Target and translates calls to the Adaptee. FishingBoatAdapter implements RowingBoat and delegates row() to boat.sail().
  • Client – The code that uses the Target interface. Captain depends on RowingBoat and remains unaware of FishingBoat.

Step-by-Step Implementation Guide

To implement Adapter pattern for legacy system integration in your own projects, follow the structural approach demonstrated in the java-design-patterns repository.

Step 1: Define the Target Interface

Create the interface that your modern client code will consume. This represents the contract you wish the legacy system could fulfill.

// File: adapter/src/main/java/com/iluwatar/adapter/RowingBoat.java
public interface RowingBoat {
    void row();
}

Step 2: Identify the Legacy Adaptee

Locate the legacy class that contains the functionality you need but exposes the wrong interface. Do not modify this class.

// File: adapter/src/main/java/com/iluwatar/adapter/FishingBoat.java
public final class FishingBoat {
    private static final Logger LOGGER = LoggerFactory.getLogger(FishingBoat.class);
    
    public void sail() {
        LOGGER.info("The fishing boat is sailing");
    }
}

Step 3: Create the Adapter Class

Implement the Target interface in a new adapter class. Use object composition to hold a reference to the legacy Adaptee, then delegate the Target method calls to the Adaptee's methods.

// File: adapter/src/main/java/com/iluwatar/adapter/FishingBoatAdapter.java
public class FishingBoatAdapter implements RowingBoat {
    private final FishingBoat boat = new FishingBoat();

    @Override
    public void row() {
        boat.sail();  // Delegation to legacy API
    }
}

Step 4: Implement the Client

Write the client code to depend only on the Target interface. This ensures the client remains decoupled from the legacy implementation.

// File: adapter/src/main/java/com/iluwatar/adapter/Captain.java
public final class Captain {
    private RowingBoat rowingBoat;

    public Captain(RowingBoat rowingBoat) {
        this.rowingBoat = rowingBoat;
    }

    public void row() {
        rowingBoat.row();
    }
}

Step 5: Assemble the Application

Wire the components together by injecting the Adapter where the Client expects the Target. This is the integration point that bridges modern and legacy code.

// File: adapter/src/main/java/com/iluwatar/adapter/App.java
public final class App {
    public static void main(String[] args) {
        // Injecting the adapter where the client expects RowingBoat
        Captain captain = new Captain(new FishingBoatAdapter());
        captain.row();  // Outputs: "The fishing boat is sailing"
    }
}

Production Strategies for Legacy System Integration

When applying the Adapter pattern to real-world enterprise scenarios, consider these advanced strategies to ensure maintainable and robust integration.

Object Composition vs. Inheritance

Always prefer object composition over class inheritance when implementing adapters. The FishingBoatAdapter holds a private instance of FishingBoat rather than extending it. This approach allows the adapter to work with any subclass of the legacy type and prevents fragile base class problems. It also enables the adapter to translate calls between entirely unrelated class hierarchies, which inheritance cannot achieve.

Dependency Injection and Framework Integration

In Spring-based applications, register your adapter as a bean to enable loose coupling through dependency injection. This allows you to swap legacy implementations or mock them for testing without changing client code.

@Component
public class LegacySystemAdapter implements NewService {
    private final LegacySystem legacy;

    public LegacySystemAdapter() {
        this.legacy = new LegacySystem();
    }

    @Override
    public Result doWork(Request req) {
        // Translate modern request to legacy format
        LegacyRequest lr = mapToLegacy(req);
        LegacyResult lrsp = legacy.perform(lr);
        // Translate legacy result back to modern format
        return mapToModern(lrsp);
    }
}

Cross-Cutting Concerns and Exception Translation

Use the adapter as a seam to inject modern concerns into legacy workflows. Since the adapter controls the boundary between new and old code, you can add logging, metrics, transaction management, or exception translation without modifying the legacy source. For example, catch legacy-specific exceptions in the adapter's methods and rethrow them as domain-specific exceptions that your modern error handling strategy understands.

Summary

  • The Adapter pattern enables integration between incompatible interfaces by introducing a middleman that translates modern API calls into legacy API calls.
  • Object composition is the preferred implementation strategy, allowing the adapter to delegate to the legacy class without inheritance constraints.
  • Key components include the Target interface (RowingBoat), the Adaptee legacy class (FishingBoat), the Adapter implementation (FishingBoatAdapter), and the Client (Captain).
  • Dependency injection frameworks like Spring can manage adapter lifecycle, making it easy to swap legacy implementations or mock them for testing.
  • Exception translation and logging can be handled within the adapter, providing a clean boundary between modern error handling strategies and legacy failure modes.

Frequently Asked Questions

What is the difference between the Adapter pattern and the Decorator pattern?

The Adapter pattern changes the interface of an existing object to match what the client expects, focusing on interface compatibility between unrelated classes. The Decorator pattern keeps the same interface but adds new responsibilities or behaviors to the object dynamically. While both use composition, the Adapter translates method signatures, whereas the Decorator enhances functionality.

Should I use object composition or class inheritance when implementing an Adapter?

You should always prefer object composition over class inheritance for Adapter implementations. Composition allows the adapter to work with any subclass of the adaptee and prevents the fragile base class problem associated with inheritance. It also enables adapting classes that are not related in the inheritance hierarchy, providing greater flexibility for legacy system integration.

Can one Adapter class wrap multiple legacy classes simultaneously?

Yes, a single Adapter can encapsulate multiple adaptees when a modern interface requires orchestrating several legacy systems to fulfill a single request. The adapter would hold references to multiple legacy objects and coordinate calls between them, translating the single modern method call into the appropriate sequence of legacy API invocations. This approach is common when modernizing monolithic operations that were previously distributed across several legacy modules.

How does the Adapter pattern support the Open/Closed Principle?

The Adapter pattern supports the Open/Closed Principle by allowing you to introduce new integrations with legacy systems without modifying existing client code or the legacy code itself. When a new legacy system needs integration, you create a new adapter class that implements the existing target interface, then inject it where the client expects that interface. This extends the system's capabilities while keeping both the client and legacy components closed to modification.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →