← All patterns

Adapter

Structural · view source on GitHub

Read this in: English · Português · Español

Category: Structural

The problem

Two pieces of code need to talk to each other, but their interfaces don't match: different method names, different parameter shapes, different error-handling conventions. The two most common reasons this happens are (a) one side is a legacy or third-party API you can't change, and (b) the "new" side was designed without knowing about the old one. Rewriting the legacy side is often not an option — it might be a mainframe system, a vendor SDK, or just code with too much blast radius to touch.

The solution

Introduce a thin wrapper that implements the interface the client expects, and translates each call into whatever the adaptee actually understands.

classDiagram
    class Target {
        <<interface>>
    }
    class Adapter {
    }
    class Adaptee {
    }
    Target <|.. Adapter
    Adapter --> Adaptee : delegates to
    Client --> Target

Classic example

classic/EnumerationIteratorAdapter is the canonical Java example of this pattern: it adapts the pre-Java-2 Enumeration contract (hasMoreElements() / nextElement()) to the modern Iterator contract (hasNext() / next()), so code written against Iterator — for-each loops, streams — can consume anything that only exposes an Enumeration. This is exactly what Collections.enumeration()'s counterpart solves in the JDK itself. EnumerationIteratorAdapterTest walks a wrapped enumeration end to end and checks it throws NoSuchElementException once exhausted, same as any other Iterator.

Applied example: fronting a mainframe account system

applied/MainframeAccountGateway stands in for a real mainframe/COBOL account system: fixed-width positional records (ACCOUNT[10] + NAME[25] + BALANCE_CENTS[10] + STATUS[1]) and a checked exception on failure — the kind of interface you actually get modernizing a decades-old core banking system, not a hypothetical one.

applied/MainframeAccountLookupAdapter exposes that gateway behind the modern AccountLookupPort contract that new microservices code depends on. New code never parses a fixed-width string or catches a checked MainframeUnavailableException — the adapter absorbs both, translating the legacy checked exception into an unchecked AccountLookupException at the boundary. This is the same shape as fronting a real mainframe during a modernization effort: the legacy system doesn't change, but nothing downstream of the adapter has to know it exists. MainframeAccountLookupAdapterTest covers record parsing, the "unknown account" sentinel record, and the exception translation.

When not to use it

Test coverage

100% instruction coverage, 100% branch coverage (JaCoCo). Reproduce it yourself:

./gradlew :structural:adapter:jacocoTestReport

Report at structural/adapter/build/reports/jacoco/test/html/index.html.

Further reading

Unit tests

src/test/java/com/designpatterns/structural/adapter/classic/EnumerationIteratorAdapterTest.java
package com.designpatterns.structural.adapter.classic;

import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class EnumerationIteratorAdapterTest {

    @Test
    void walksEveryElementOfTheAdaptedEnumeration() {
        Enumeration<String> legacyEnumeration = Collections.enumeration(List.of("PIX", "TED", "BOLETO"));
        Iterator<String> iterator = new EnumerationIteratorAdapter<>(legacyEnumeration);

        assertThat(iterator).toIterable().containsExactly("PIX", "TED", "BOLETO");
    }

    @Test
    void throwsNoSuchElementExceptionOnceExhausted() {
        Enumeration<String> emptyEnumeration = Collections.enumeration(List.of());
        Iterator<String> iterator = new EnumerationIteratorAdapter<>(emptyEnumeration);

        assertThat(iterator.hasNext()).isFalse();
        assertThatThrownBy(iterator::next).isInstanceOf(NoSuchElementException.class);
    }
}
src/test/java/com/designpatterns/structural/adapter/applied/MainframeAccountLookupAdapterTest.java
package com.designpatterns.structural.adapter.applied;

import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class MainframeAccountLookupAdapterTest {

    private static final String ACCOUNT_NUMBER = "1234567";
    private static final String PADDED_ACCOUNT_NUMBER = "0001234567";

    @Test
    void parsesTheLegacyFixedWidthRecordIntoAModernSnapshot() {
        String record = PADDED_ACCOUNT_NUMBER + leftJustify("JOAO DA SILVA", 25) + "0000015000" + "A";
        MainframeAccountGateway gateway = new MainframeAccountGateway(Map.of(PADDED_ACCOUNT_NUMBER, record));
        MainframeAccountLookupAdapter adapter = new MainframeAccountLookupAdapter(gateway);

        AccountSnapshot snapshot = adapter.findByAccountNumber(ACCOUNT_NUMBER);

        assertThat(snapshot.accountNumber()).isEqualTo(PADDED_ACCOUNT_NUMBER);
        assertThat(snapshot.holderName()).isEqualTo("JOAO DA SILVA");
        assertThat(snapshot.balanceCents()).isEqualTo(15_000L);
        assertThat(snapshot.active()).isTrue();
    }

    @Test
    void returnsAnInactiveZeroBalanceSnapshotWhenTheAccountIsUnknown() {
        MainframeAccountGateway gateway = new MainframeAccountGateway(Map.of());
        MainframeAccountLookupAdapter adapter = new MainframeAccountLookupAdapter(gateway);

        AccountSnapshot snapshot = adapter.findByAccountNumber(ACCOUNT_NUMBER);

        assertThat(snapshot.balanceCents()).isZero();
        assertThat(snapshot.active()).isFalse();
    }

    @Test
    void translatesTheLegacyCheckedExceptionIntoAnUncheckedLookupException() {
        MainframeAccountGateway gateway = new MainframeAccountGateway(Map.of());
        MainframeAccountLookupAdapter adapter = new MainframeAccountLookupAdapter(gateway);

        assertThatThrownBy(() -> adapter.findByAccountNumber("9999999999"))
                .isInstanceOf(AccountLookupException.class)
                .hasCauseInstanceOf(MainframeUnavailableException.class);
    }

    private static String leftJustify(String value, int width) {
        return String.format("%-" + width + "s", value);
    }
}

View full JaCoCo coverage report →