Mediumstructural15 min read

Adapter Design Pattern

1. What is Adapter Pattern?

Definition

The Adapter pattern is a GOF Structural Pattern. It makes two incompatible components or interfaces interact with each other by performing the necessary conversions. It acts as a bridge between two incompatible interfaces.

One line: Convert the interface of one class into another interface the client expects, so that classes that could not work together can collaborate.

An adapter is responsible for two things:

  1. Performing data conversions as required for the destination (e.g., city name to city code, Fahrenheit to Celsius)
  2. Interacting with the destination component by giving it the necessary inputs in the format it expects

Real-world examples

Example 1: Power Adapter Live Supply (230V AC) --> [Adapter] --> Electronic Device (DC 9-12V) (Indian outlet) 230V AC (Mobile phone, laptop) to DC The adapter converts 230V AC to low-voltage DC. For an electronic product purchased in USA to work in India, we need an adapter which takes 230V and converts to 120V. Then that 120V will be converted to DC power.
Example 2: Weather App Your App Adapter External Component (uses city name, --> (converts) --> WeatherReport API needs celsius) city name (needs city code, to city code returns fahrenheit) fahrenheit to celsius
Example 3: Stock Trading StockBroker Adapter StockExchange (stock name, --> (converts) --> (stock code, stock price) name to code numeric price) Example 4: E-Commerce E-Commerce Store Adapter Payment Gateway (bank name, --> (converts) --> (bank code, card details) name to code encrypted token)

2. The Problem

Problem

Your application (local component) uses city names and expects temperature in Celsius. The external WeatherReport API uses city codes (integers) and returns temperature in Fahrenheit. The two components are incompatible. Your app cannot call the external API directly because the data formats do not match.

Problem: Incompatible Interfaces Your App (Local) External API (WeatherReport) +-------------------+ +-------------------+ | Input: "Hyderabad"| | Input: 1100 | | (city name) | INCOMPATIBLE | (city code) | | Output: celsius | <--------- | Output: fahrenheit| +-------------------+ cannot call +-------------------+ The external comp is expecting city code, not the city name. Similarly, external comp is giving temperature in fahrenheit degrees but we need it in celsius degrees. For this, take the support of Adapter (two-way adapter).

3. The Solution: Adapter as Bridge

Solution

Place an Adapter class between your local component and the external component. The adapter:

  1. Implements your local interface (so your app can call it normally)
  2. Internally creates/holds an instance of the external component
  3. Converts input data from your format to the external format (city name to city code)
  4. Calls the external component with the converted data
  5. Converts the output data from external format to your format (Fahrenheit to Celsius)
  6. Returns the converted result to your app
Solution: Adapter bridges the gap Your App --> IAccuWeatherService --> Adapter --> IWeatherReportComp --> External API (city name) (your interface) (converts) (external interface) (city code) (celsius) both ways (fahrenheit)

4. AccuWeather Example (from course notes)

The AccuWeather Local Service Component interacts with the destination WeatherReport component through an adapter that performs two conversions: (a) converting city name to city code, and (b) converting temperature from Fahrenheit degrees to Celsius degrees.

External component (third-party, cannot modify)

// IWeatherReportComp.java (external interface)
package com.nt.external;

public interface IWeatherReportComp {
    float getTemperature(int cityCode);
}

// WeatherReportCompImpl.java (external implementation)
package com.nt.external;

import java.util.Random;

public class WeatherReportCompImpl implements IWeatherReportComp {
    @Override
    public float getTemperature(int cityCode) {
        // Returns temperature in FAHRENHEIT based on city code
        if (cityCode == 1100)       // Hyderabad - India
            return new Random().nextFloat() * 100.0f;
        else if (cityCode == 1101)  // Delhi
            return new Random().nextFloat() * 100.0f;
        else if (cityCode == 1202)  // Bangalore
            return new Random().nextFloat() * 100.0f;
        else
            throw new IllegalArgumentException("Invalid city code");
    }
}

Local service interface (your application)

// IAccuWeatherService.java (local interface)
package com.nt.service;

public interface IAccuWeatherService {
    String showTemperature(String cityName);
}

Adapter class (the bridge)

// WeatherReportCompAdapter.java
package com.nt.adapter;

import com.nt.external.IWeatherReportComp;
import com.nt.external.WeatherReportCompImpl;

public class WeatherReportCompAdapter {

    public String fetchTemperature(String cityName) {
        // Conversion 1: city name to city code (adapter logic)
        int cityCode;
        if (cityName.equalsIgnoreCase("hyderabad"))
            cityCode = 1100;
        else if (cityName.equalsIgnoreCase("bangalore"))
            cityCode = 1202;
        else if (cityName.equalsIgnoreCase("delhi"))
            cityCode = 1101;
        else
            return "City not found in list";

        // Use external comp service
        IWeatherReportComp comp = new WeatherReportCompImpl();
        float tempF = comp.getTemperature(cityCode);

        // Conversion 2: fahrenheit to celsius (adapter logic)
        float tempC = (5.0f / 9.0f) * (tempF - 32.0f);
        return tempC + " celsius degrees";
    }
}

Local service implementation (uses adapter internally)

// AccuWeatherServiceImpl.java
package com.nt.service;

import com.nt.adapter.WeatherReportCompAdapter;

public class AccuWeatherServiceImpl implements IAccuWeatherService {

    @Override
    public String showTemperature(String cityName) {
        // Use adapter class
        WeatherReportCompAdapter adapter = new WeatherReportCompAdapter();
        String tempInfo = adapter.fetchTemperature(cityName);
        return tempInfo;
    }
}

Client test

// AdapterDPTest.java
package com.nt.test;

import com.nt.service.AccuWeatherServiceImpl;
import com.nt.service.IAccuWeatherService;

public class AdapterDPTest {
    public static void main(String[] args) {
        // Use AccuWeather service
        IAccuWeatherService service = new AccuWeatherServiceImpl();
        String result = service.showTemperature("hyderabad");
        System.out.println("Temperature: " + result);
    }
}
Temperature: 22.35 celsius degrees

5. Two Types of Adapter

a) Object Adapter (Composition - Recommended)

The adapter holds a reference to the adaptee (external component) via composition (HAS-A).

// Object Adapter: holds reference to adaptee
public class WeatherAdapter implements IAccuWeatherService {
    private final IWeatherReportComp adaptee;  // HAS-A (composition)

    public WeatherAdapter(IWeatherReportComp adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public String showTemperature(String cityName) {
        int cityCode = convertCityNameToCode(cityName);
        float tempF = adaptee.getTemperature(cityCode);
        float tempC = fahrenheitToCelsius(tempF);
        return String.format("%.1f celsius", tempC);
    }

    private int convertCityNameToCode(String name) { /* ... */ }
    private float fahrenheitToCelsius(float f) { return (5.0f / 9.0f) * (f - 32); }
}

b) Class Adapter (Inheritance - Less common in Java)

The adapter extends the adaptee class and implements the target interface. Requires multiple inheritance, so in Java this is only possible when the adaptee is a concrete class (extend) and the target is an interface (implement).

// Class Adapter: extends adaptee, implements target
public class WeatherClassAdapter extends WeatherReportCompImpl
                                  implements IAccuWeatherService {

    @Override
    public String showTemperature(String cityName) {
        int cityCode = convertCityNameToCode(cityName);
        float tempF = getTemperature(cityCode);  // inherited from adaptee
        float tempC = fahrenheitToCelsius(tempF);
        return String.format("%.1f celsius", tempC);
    }
}
AspectObject AdapterClass Adapter
MechanismComposition (HAS-A)Inheritance (IS-A)
FlexibilityCan adapt any subclass of adapteeLocked to one specific adaptee class
Java supportFully supportedLimited (no multiple class inheritance)
RecommendedYesUse only when composition is not possible

6. Pre-defined Adapter Classes in Java

AdapterConverts FromConverts To
java.util.Arrays.asList()ArrayList (gives List from given array content)
java.util.Collections.list()EnumerationArrayList (gives collection from the given content)
java.util.Collections.enumeration()CollectionEnumeration (appears in Enumerations)
java.io.InputStreamReaderInputStream (byte stream)Reader (character stream)
java.io.OutputStreamWriterOutputStream (byte stream)Writer (character stream)
javax.xml.bind.annotation.adapters.XmlAdapterXML data (marshalling)Java object data (unmarshalling). Used to convert XML data to object data and object data to XML data.

7. Full-Length Industry Project: Currency Exchange Adapter

Business Scenario

National Bank of India's international transfer service needs to display exchange rates and convert currencies. The bank's internal system uses INR and city names. The external forex API (ExchangeRateAPI) uses ISO currency codes (USD, EUR, GBP) and returns rates in a different response format. An adapter bridges the gap so the bank's service can call the external API without knowing its internal format.

High-Level Design (HLD)

+---------------------------------------------------+ | BANK SERVICE LAYER | | CurrencyService (uses ICurrencyProvider) | | - getExchangeRate("US Dollar") | | - convert(1000, "INR", "US Dollar") | | (uses currency NAMES, expects INR-based rates) | +----------------------------+----------------------+ | v +---------------------------------------------------+ | ADAPTER LAYER | | ForexAPIAdapter (implements ICurrencyProvider) | | - converts currency NAME to ISO CODE | | - calls external API with ISO code | | - converts response format to bank format | +----------------------------+----------------------+ | v +---------------------------------------------------+ | EXTERNAL API LAYER (cannot modify) | | IForexAPI interface | | ForexAPIImpl | | - getRate(String isoCode): ExternalRate | | (uses ISO codes: USD, EUR, GBP, JPY) | | (returns rate in its own format) | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | ICurrencyProvider (bank's interface) | +-------------------------------------------------------+ | + getExchangeRate(currencyName): ExchangeRate | | + convert(amount, from, to): ConversionResult | | + getSupportedCurrencies(): List<String> | +-------------------------------------------------------+ ^ | +-------------------------------------------------------+ | ForexAPIAdapter (implements ICurrencyProvider) | +-------------------------------------------------------+ | - forexAPI: IForexAPI [HAS-A, composition] | | - nameToCodeMap: Map [conversion lookup] | +-------------------------------------------------------+ | + getExchangeRate(name): ExchangeRate | | + convert(amount, from, to): ConversionResult | | - resolveCode(name): String [name to ISO code] | | - toExchangeRate(ext): ExchangeRate [format convert] | +-------------------------------------------------------+ +-------------------------------------------------------+ | IForexAPI (external, cannot modify) | +-------------------------------------------------------+ | + getRate(isoCode): ExternalRate | +-------------------------------------------------------+ ^ | +-------------------------------------------------------+ | ForexAPIImpl (external implementation) | +-------------------------------------------------------+ | + getRate(isoCode): ExternalRate | | (returns rate relative to USD, in external format) | +-------------------------------------------------------+ +------------------------------+ +------------------------------+ | ExternalRate (external model)| | ExchangeRate (bank model) | +------------------------------+ +------------------------------+ | - isoCode: String | | - currencyName: String | | - rateToUSD: double | | - isoCode: String | | - lastUpdated: long | | - rateToINR: double | +------------------------------+ | - lastUpdated: String | +------------------------------+ +------------------------------+ | ConversionResult | +------------------------------+ | - fromCurrency: String | | - toCurrency: String | | - originalAmount: double | | - convertedAmount: double | | - rate: double | +------------------------------+

Project Structure

nbi-currency-adapter/ ├── src/ │ └── main/java/com/nbi/currency/ │ ├── external/ │ │ ├── IForexAPI.java (external interface, cannot modify) │ │ ├── ForexAPIImpl.java (external implementation) │ │ └── ExternalRate.java (external model) │ ├── model/ │ │ ├── ExchangeRate.java (bank's model) │ │ └── ConversionResult.java │ ├── adapter/ │ │ └── ForexAPIAdapter.java (adapter) │ ├── provider/ │ │ └── ICurrencyProvider.java (bank's interface) │ ├── service/ │ │ └── CurrencyService.java │ └── Application.java ├── src/ │ └── test/java/com/nbi/currency/ │ └── adapter/ │ └── ForexAPIAdapterTest.java └── pom.xml

External API (third-party, cannot modify)

// ExternalRate.java (external model)
package com.nbi.currency.external;

public class ExternalRate {
    private final String isoCode;
    private final double rateToUSD;
    private final long lastUpdatedEpoch;

    public ExternalRate(String isoCode, double rateToUSD, long lastUpdatedEpoch) {
        this.isoCode = isoCode;
        this.rateToUSD = rateToUSD;
        this.lastUpdatedEpoch = lastUpdatedEpoch;
    }

    public String getIsoCode() { return isoCode; }
    public double getRateToUSD() { return rateToUSD; }
    public long getLastUpdatedEpoch() { return lastUpdatedEpoch; }
}

// IForexAPI.java (external interface)
package com.nbi.currency.external;

public interface IForexAPI {
    ExternalRate getRate(String isoCode);
}

// ForexAPIImpl.java (external implementation)
package com.nbi.currency.external;

public class ForexAPIImpl implements IForexAPI {

    @Override
    public ExternalRate getRate(String isoCode) {
        // Simulated rates (1 unit of currency = X USD)
        long now = System.currentTimeMillis();
        switch (isoCode.toUpperCase()) {
            case "INR": return new ExternalRate("INR", 0.012, now);
            case "USD": return new ExternalRate("USD", 1.0, now);
            case "EUR": return new ExternalRate("EUR", 1.08, now);
            case "GBP": return new ExternalRate("GBP", 1.26, now);
            case "JPY": return new ExternalRate("JPY", 0.0067, now);
            case "AED": return new ExternalRate("AED", 0.27, now);
            default:
                throw new IllegalArgumentException("Unsupported currency: " + isoCode);
        }
    }
}

Bank's model and interface

// ExchangeRate.java (bank's model)
package com.nbi.currency.model;

public class ExchangeRate {
    private final String currencyName;
    private final String isoCode;
    private final double rateToINR;
    private final String lastUpdated;

    public ExchangeRate(String currencyName, String isoCode,
                        double rateToINR, String lastUpdated) {
        this.currencyName = currencyName;
        this.isoCode = isoCode;
        this.rateToINR = rateToINR;
        this.lastUpdated = lastUpdated;
    }

    public String getCurrencyName() { return currencyName; }
    public String getIsoCode() { return isoCode; }
    public double getRateToINR() { return rateToINR; }
    public String getLastUpdated() { return lastUpdated; }

    @Override
    public String toString() {
        return String.format("1 %s (%s) = Rs %.2f INR [updated: %s]",
            currencyName, isoCode, rateToINR, lastUpdated);
    }
}

// ConversionResult.java
package com.nbi.currency.model;

public class ConversionResult {
    private final String fromCurrency;
    private final String toCurrency;
    private final double originalAmount;
    private final double convertedAmount;
    private final double rate;

    public ConversionResult(String fromCurrency, String toCurrency,
                            double originalAmount, double convertedAmount, double rate) {
        this.fromCurrency = fromCurrency;
        this.toCurrency = toCurrency;
        this.originalAmount = originalAmount;
        this.convertedAmount = convertedAmount;
        this.rate = rate;
    }

    public double getConvertedAmount() { return convertedAmount; }

    @Override
    public String toString() {
        return String.format("%.2f %s = %.2f %s (rate: %.4f)",
            originalAmount, fromCurrency, convertedAmount, toCurrency, rate);
    }
}

// ICurrencyProvider.java (bank's interface)
package com.nbi.currency.provider;

import com.nbi.currency.model.ExchangeRate;
import com.nbi.currency.model.ConversionResult;
import java.util.List;

public interface ICurrencyProvider {
    ExchangeRate getExchangeRate(String currencyName);
    ConversionResult convert(double amount, String fromCurrency, String toCurrency);
    List<String> getSupportedCurrencies();
}

ForexAPIAdapter.java (the adapter)

package com.nbi.currency.adapter;

import com.nbi.currency.external.ExternalRate;
import com.nbi.currency.external.ForexAPIImpl;
import com.nbi.currency.external.IForexAPI;
import com.nbi.currency.model.ConversionResult;
import com.nbi.currency.model.ExchangeRate;
import com.nbi.currency.provider.ICurrencyProvider;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;

public class ForexAPIAdapter implements ICurrencyProvider {
    private final IForexAPI forexAPI;  // HAS-A (composition with external component)

    private static final Map<String, String> NAME_TO_CODE = new LinkedHashMap<>();
    static {
        NAME_TO_CODE.put("Indian Rupee", "INR");
        NAME_TO_CODE.put("US Dollar", "USD");
        NAME_TO_CODE.put("Euro", "EUR");
        NAME_TO_CODE.put("British Pound", "GBP");
        NAME_TO_CODE.put("Japanese Yen", "JPY");
        NAME_TO_CODE.put("UAE Dirham", "AED");
    }

    private static final DateTimeFormatter FORMATTER =
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
            .withZone(ZoneId.of("Asia/Kolkata"));

    public ForexAPIAdapter() {
        this.forexAPI = new ForexAPIImpl();
    }

    // Constructor for testing with mock API
    public ForexAPIAdapter(IForexAPI forexAPI) {
        this.forexAPI = forexAPI;
    }

    @Override
    public ExchangeRate getExchangeRate(String currencyName) {
        // Conversion 1: currency name to ISO code
        String isoCode = resolveCode(currencyName);

        // Call external API with ISO code
        ExternalRate externalRate = forexAPI.getRate(isoCode);

        // Conversion 2: USD-based rate to INR-based rate
        ExternalRate inrRate = forexAPI.getRate("INR");
        double rateToINR = externalRate.getRateToUSD() / inrRate.getRateToUSD();

        // Conversion 3: epoch timestamp to readable date
        String lastUpdated = FORMATTER.format(
            Instant.ofEpochMilli(externalRate.getLastUpdatedEpoch()));

        return new ExchangeRate(currencyName, isoCode, rateToINR, lastUpdated);
    }

    @Override
    public ConversionResult convert(double amount, String fromCurrency, String toCurrency) {
        ExchangeRate fromRate = getExchangeRate(fromCurrency);
        ExchangeRate toRate = getExchangeRate(toCurrency);

        // Convert: from -> INR -> to
        double amountInINR = amount * fromRate.getRateToINR();
        double convertedAmount = amountInINR / toRate.getRateToINR();
        double directRate = fromRate.getRateToINR() / toRate.getRateToINR();

        return new ConversionResult(fromCurrency, toCurrency,
            amount, convertedAmount, directRate);
    }

    @Override
    public List<String> getSupportedCurrencies() {
        return new ArrayList<>(NAME_TO_CODE.keySet());
    }

    private String resolveCode(String currencyName) {
        String code = NAME_TO_CODE.get(currencyName);
        if (code == null) {
            throw new IllegalArgumentException(
                "Unknown currency: " + currencyName
                + ". Supported: " + NAME_TO_CODE.keySet());
        }
        return code;
    }
}

CurrencyService.java

package com.nbi.currency.service;

import com.nbi.currency.adapter.ForexAPIAdapter;
import com.nbi.currency.model.ConversionResult;
import com.nbi.currency.model.ExchangeRate;
import com.nbi.currency.provider.ICurrencyProvider;

public class CurrencyService {
    private final ICurrencyProvider provider;

    public CurrencyService() {
        this.provider = new ForexAPIAdapter();
    }

    // Constructor for testing with mock provider
    public CurrencyService(ICurrencyProvider provider) {
        this.provider = provider;
    }

    public void displayRate(String currencyName) {
        ExchangeRate rate = provider.getExchangeRate(currencyName);
        System.out.println("  " + rate);
    }

    public ConversionResult convertCurrency(double amount,
                                             String from, String to) {
        ConversionResult result = provider.convert(amount, from, to);
        System.out.println("  " + result);
        return result;
    }
}

Application.java

package com.nbi.currency;

import com.nbi.currency.service.CurrencyService;

public class Application {
    public static void main(String[] args) {
        CurrencyService service = new CurrencyService();

        // Scenario 1: Display exchange rates
        System.out.println("=== Scenario 1: Exchange Rates ===");
        service.displayRate("US Dollar");
        service.displayRate("Euro");
        service.displayRate("British Pound");
        service.displayRate("Japanese Yen");
        service.displayRate("UAE Dirham");
        System.out.println();

        // Scenario 2: Convert INR to USD
        System.out.println("=== Scenario 2: INR to USD ===");
        service.convertCurrency(100000, "Indian Rupee", "US Dollar");
        System.out.println();

        // Scenario 3: Convert USD to EUR
        System.out.println("=== Scenario 3: USD to EUR ===");
        service.convertCurrency(5000, "US Dollar", "Euro");
        System.out.println();

        // Scenario 4: Convert GBP to INR
        System.out.println("=== Scenario 4: GBP to INR ===");
        service.convertCurrency(1000, "British Pound", "Indian Rupee");
        System.out.println();

        // Scenario 5: Invalid currency
        System.out.println("=== Scenario 5: Invalid Currency ===");
        try {
            service.displayRate("Bitcoin");
        } catch (IllegalArgumentException e) {
            System.out.println("  Error: " + e.getMessage());
        }
        System.out.println();

        // Scenario 6: List supported currencies
        System.out.println("=== Scenario 6: Supported Currencies ===");
        var adapter = new com.nbi.currency.adapter.ForexAPIAdapter();
        System.out.println("  " + adapter.getSupportedCurrencies());
    }
}

Expected Output

=== Scenario 1: Exchange Rates === 1 US Dollar (USD) = Rs 83.33 INR [updated: 2026-11-02 18:00:00] 1 Euro (EUR) = Rs 90.00 INR [updated: 2026-11-02 18:00:00] 1 British Pound (GBP) = Rs 105.00 INR [updated: 2026-11-02 18:00:00] 1 Japanese Yen (JPY) = Rs 0.56 INR [updated: 2026-11-02 18:00:00] 1 UAE Dirham (AED) = Rs 22.50 INR [updated: 2026-11-02 18:00:00] === Scenario 2: INR to USD === 100000.00 Indian Rupee = 1200.00 US Dollar (rate: 0.0120) === Scenario 3: USD to EUR === 5000.00 US Dollar = 4629.63 Euro (rate: 0.9259) === Scenario 4: GBP to INR === 1000.00 British Pound = 105000.00 Indian Rupee (rate: 105.0000) === Scenario 5: Invalid Currency === Error: Unknown currency: Bitcoin. Supported: [Indian Rupee, US Dollar, Euro, British Pound, Japanese Yen, UAE Dirham] === Scenario 6: Supported Currencies === [Indian Rupee, US Dollar, Euro, British Pound, Japanese Yen, UAE Dirham]

ForexAPIAdapterTest.java

package com.nbi.currency.adapter;

import com.nbi.currency.external.ExternalRate;
import com.nbi.currency.external.IForexAPI;
import com.nbi.currency.model.ConversionResult;
import com.nbi.currency.model.ExchangeRate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class ForexAPIAdapterTest {
    private ForexAPIAdapter adapter;

    @BeforeEach
    void setUp() {
        // Mock external API with fixed rates for predictable testing
        IForexAPI mockAPI = (isoCode) -> {
            switch (isoCode) {
                case "INR": return new ExternalRate("INR", 0.012, System.currentTimeMillis());
                case "USD": return new ExternalRate("USD", 1.0, System.currentTimeMillis());
                case "EUR": return new ExternalRate("EUR", 1.08, System.currentTimeMillis());
                case "GBP": return new ExternalRate("GBP", 1.26, System.currentTimeMillis());
                default: throw new IllegalArgumentException("Unknown: " + isoCode);
            }
        };
        adapter = new ForexAPIAdapter(mockAPI);
    }

    @Test
    void getExchangeRate_convertsNameToCode() {
        ExchangeRate rate = adapter.getExchangeRate("US Dollar");
        assertEquals("USD", rate.getIsoCode());
    }

    @Test
    void getExchangeRate_convertsToINR() {
        ExchangeRate rate = adapter.getExchangeRate("US Dollar");
        assertTrue(rate.getRateToINR() > 0);
        // 1 USD = 1.0/0.012 = ~83.33 INR
        assertEquals(83.33, rate.getRateToINR(), 1.0);
    }

    @Test
    void getExchangeRate_formatsDate() {
        ExchangeRate rate = adapter.getExchangeRate("Euro");
        assertNotNull(rate.getLastUpdated());
        assertTrue(rate.getLastUpdated().contains("2026"));
    }

    @Test
    void convert_INRtoUSD() {
        ConversionResult result = adapter.convert(83333, "Indian Rupee", "US Dollar");
        assertEquals(1000.0, result.getConvertedAmount(), 5.0);
    }

    @Test
    void convert_USDtoEUR() {
        ConversionResult result = adapter.convert(1000, "US Dollar", "Euro");
        assertTrue(result.getConvertedAmount() > 0);
    }

    @Test
    void getExchangeRate_throwsForUnknown() {
        assertThrows(IllegalArgumentException.class,
            () -> adapter.getExchangeRate("Bitcoin"));
    }

    @Test
    void getSupportedCurrencies_returnsAll() {
        var currencies = adapter.getSupportedCurrencies();
        assertTrue(currencies.contains("US Dollar"));
        assertTrue(currencies.contains("Indian Rupee"));
        assertTrue(currencies.contains("Euro"));
    }

    @Test
    void adapter_usesComposition() {
        // Adapter should work with any IForexAPI implementation
        // (mock or real). This proves Object Adapter pattern.
        assertNotNull(adapter.getExchangeRate("British Pound"));
    }
}

Key observations from the project

  1. Three conversions performed by the adapter: Currency name to ISO code, USD-based rate to INR-based rate, epoch timestamp to human-readable date. The bank service never knows about ISO codes or USD base rates.
  2. Object Adapter pattern: ForexAPIAdapter holds IForexAPI reference (HAS-A composition). This allows injecting a mock API for testing.
  3. External API is untouched: IForexAPI and ForexAPIImpl are not modified. The adapter wraps them without changing their code.
  4. CurrencyService uses only the bank's interface: It depends on ICurrencyProvider, not on the adapter directly. Tomorrow, if the external API changes, only the adapter changes.
  5. Testable with mocks: The test creates a mock IForexAPI with fixed rates, injects it into the adapter via constructor, and verifies conversions with predictable values.

8. When to Use and When NOT to Use

When to use Adapter

ScenarioExampleWhy Adapter
Integrating third-party APIsWeather API, Payment Gateway, SMS serviceExternal API has a different interface than your system expects
Legacy system integrationOld SOAP service with new REST clientConvert between old and new data formats
Data format conversionXML to JSON, CSV to database recordsSource and destination use different formats
Unit conversionsFahrenheit to Celsius, miles to kmExternal system uses different units
Wrapping vendor librariesSwitching from one logging lib to anotherAdapter isolates your code from vendor-specific APIs

When NOT to use Adapter

ScenarioWhy NOTUse Instead
Interfaces are already compatibleNo conversion neededDirect usage
You control both sidesModify the source interface insteadRefactor the interface
Too many conversionsAdapter becomes a god class doing everythingBreak into multiple focused adapters

9. Quick Reference Card

ComponentRoleIn Currency Project
Target InterfaceThe interface your client expectsICurrencyProvider
AdapteeThe existing class with incompatible interfaceIForexAPI + ForexAPIImpl
AdapterConverts between target and adapteeForexAPIAdapter
ClientUses the target interfaceCurrencyService
Structural GOF Pattern Medium

Adapter Design Pattern in Java

Complete Tutorial | 10 Sections | Full Currency Exchange Project with HLD and LLD | AccuWeather Example | Class vs Object Adapter | Multiple Scenarios | 12 Interview Questions

1. What is Adapter Pattern?

Definition

The Adapter pattern is a GOF Structural Pattern. It makes two incompatible components or interfaces interact with each other by performing the necessary conversions. It acts as a bridge between two incompatible interfaces.

One line: Convert the interface of one class into another interface the client expects, so that classes that could not work together can collaborate.

An adapter is responsible for two things:

  1. Performing data conversions as required for the destination (e.g., city name to city code, Fahrenheit to Celsius)
  2. Interacting with the destination component by giving it the necessary inputs in the format it expects

Real-world examples

Example 1: Power Adapter Live Supply (230V AC) --> [Adapter] --> Electronic Device (DC 9-12V) (Indian outlet) 230V AC (Mobile phone, laptop) to DC The adapter converts 230V AC to low-voltage DC. For an electronic product purchased in USA to work in India, we need an adapter which takes 230V and converts to 120V. Then that 120V will be converted to DC power.
Example 2: Weather App Your App Adapter External Component (uses city name, --> (converts) --> WeatherReport API needs celsius) city name (needs city code, to city code returns fahrenheit) fahrenheit to celsius
Example 3: Stock Trading StockBroker Adapter StockExchange (stock name, --> (converts) --> (stock code, stock price) name to code numeric price) Example 4: E-Commerce E-Commerce Store Adapter Payment Gateway (bank name, --> (converts) --> (bank code, card details) name to code encrypted token)

2. The Problem

Problem

Your application (local component) uses city names and expects temperature in Celsius. The external WeatherReport API uses city codes (integers) and returns temperature in Fahrenheit. The two components are incompatible. Your app cannot call the external API directly because the data formats do not match.

Problem: Incompatible Interfaces Your App (Local) External API (WeatherReport) +-------------------+ +-------------------+ | Input: "Hyderabad"| | Input: 1100 | | (city name) | INCOMPATIBLE | (city code) | | Output: celsius | <--------- | Output: fahrenheit| +-------------------+ cannot call +-------------------+ The external comp is expecting city code, not the city name. Similarly, external comp is giving temperature in fahrenheit degrees but we need it in celsius degrees. For this, take the support of Adapter (two-way adapter).

3. The Solution: Adapter as Bridge

Solution

Place an Adapter class between your local component and the external component. The adapter:

  1. Implements your local interface (so your app can call it normally)
  2. Internally creates/holds an instance of the external component
  3. Converts input data from your format to the external format (city name to city code)
  4. Calls the external component with the converted data
  5. Converts the output data from external format to your format (Fahrenheit to Celsius)
  6. Returns the converted result to your app
Solution: Adapter bridges the gap Your App --> IAccuWeatherService --> Adapter --> IWeatherReportComp --> External API (city name) (your interface) (converts) (external interface) (city code) (celsius) both ways (fahrenheit)

4. AccuWeather Example (from course notes)

The AccuWeather Local Service Component interacts with the destination WeatherReport component through an adapter that performs two conversions: (a) converting city name to city code, and (b) converting temperature from Fahrenheit degrees to Celsius degrees.

External component (third-party, cannot modify)

// IWeatherReportComp.java (external interface)
package com.nt.external;

public interface IWeatherReportComp {
    float getTemperature(int cityCode);
}

// WeatherReportCompImpl.java (external implementation)
package com.nt.external;

import java.util.Random;

public class WeatherReportCompImpl implements IWeatherReportComp {
    @Override
    public float getTemperature(int cityCode) {
        // Returns temperature in FAHRENHEIT based on city code
        if (cityCode == 1100)       // Hyderabad - India
            return new Random().nextFloat() * 100.0f;
        else if (cityCode == 1101)  // Delhi
            return new Random().nextFloat() * 100.0f;
        else if (cityCode == 1202)  // Bangalore
            return new Random().nextFloat() * 100.0f;
        else
            throw new IllegalArgumentException("Invalid city code");
    }
}

Local service interface (your application)

// IAccuWeatherService.java (local interface)
package com.nt.service;

public interface IAccuWeatherService {
    String showTemperature(String cityName);
}

Adapter class (the bridge)

// WeatherReportCompAdapter.java
package com.nt.adapter;

import com.nt.external.IWeatherReportComp;
import com.nt.external.WeatherReportCompImpl;

public class WeatherReportCompAdapter {

    public String fetchTemperature(String cityName) {
        // Conversion 1: city name to city code (adapter logic)
        int cityCode;
        if (cityName.equalsIgnoreCase("hyderabad"))
            cityCode = 1100;
        else if (cityName.equalsIgnoreCase("bangalore"))
            cityCode = 1202;
        else if (cityName.equalsIgnoreCase("delhi"))
            cityCode = 1101;
        else
            return "City not found in list";

        // Use external comp service
        IWeatherReportComp comp = new WeatherReportCompImpl();
        float tempF = comp.getTemperature(cityCode);

        // Conversion 2: fahrenheit to celsius (adapter logic)
        float tempC = (5.0f / 9.0f) * (tempF - 32.0f);
        return tempC + " celsius degrees";
    }
}

Local service implementation (uses adapter internally)

// AccuWeatherServiceImpl.java
package com.nt.service;

import com.nt.adapter.WeatherReportCompAdapter;

public class AccuWeatherServiceImpl implements IAccuWeatherService {

    @Override
    public String showTemperature(String cityName) {
        // Use adapter class
        WeatherReportCompAdapter adapter = new WeatherReportCompAdapter();
        String tempInfo = adapter.fetchTemperature(cityName);
        return tempInfo;
    }
}

Client test

// AdapterDPTest.java
package com.nt.test;

import com.nt.service.AccuWeatherServiceImpl;
import com.nt.service.IAccuWeatherService;

public class AdapterDPTest {
    public static void main(String[] args) {
        // Use AccuWeather service
        IAccuWeatherService service = new AccuWeatherServiceImpl();
        String result = service.showTemperature("hyderabad");
        System.out.println("Temperature: " + result);
    }
}
Temperature: 22.35 celsius degrees

5. Two Types of Adapter

a) Object Adapter (Composition - Recommended)

The adapter holds a reference to the adaptee (external component) via composition (HAS-A).

// Object Adapter: holds reference to adaptee
public class WeatherAdapter implements IAccuWeatherService {
    private final IWeatherReportComp adaptee;  // HAS-A (composition)

    public WeatherAdapter(IWeatherReportComp adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public String showTemperature(String cityName) {
        int cityCode = convertCityNameToCode(cityName);
        float tempF = adaptee.getTemperature(cityCode);
        float tempC = fahrenheitToCelsius(tempF);
        return String.format("%.1f celsius", tempC);
    }

    private int convertCityNameToCode(String name) { /* ... */ }
    private float fahrenheitToCelsius(float f) { return (5.0f / 9.0f) * (f - 32); }
}

b) Class Adapter (Inheritance - Less common in Java)

The adapter extends the adaptee class and implements the target interface. Requires multiple inheritance, so in Java this is only possible when the adaptee is a concrete class (extend) and the target is an interface (implement).

// Class Adapter: extends adaptee, implements target
public class WeatherClassAdapter extends WeatherReportCompImpl
                                  implements IAccuWeatherService {

    @Override
    public String showTemperature(String cityName) {
        int cityCode = convertCityNameToCode(cityName);
        float tempF = getTemperature(cityCode);  // inherited from adaptee
        float tempC = fahrenheitToCelsius(tempF);
        return String.format("%.1f celsius", tempC);
    }
}
AspectObject AdapterClass Adapter
MechanismComposition (HAS-A)Inheritance (IS-A)
FlexibilityCan adapt any subclass of adapteeLocked to one specific adaptee class
Java supportFully supportedLimited (no multiple class inheritance)
RecommendedYesUse only when composition is not possible

6. Pre-defined Adapter Classes in Java

AdapterConverts FromConverts To
java.util.Arrays.asList()ArrayList (gives List from given array content)
java.util.Collections.list()EnumerationArrayList (gives collection from the given content)
java.util.Collections.enumeration()CollectionEnumeration (appears in Enumerations)
java.io.InputStreamReaderInputStream (byte stream)Reader (character stream)
java.io.OutputStreamWriterOutputStream (byte stream)Writer (character stream)
javax.xml.bind.annotation.adapters.XmlAdapterXML data (marshalling)Java object data (unmarshalling). Used to convert XML data to object data and object data to XML data.

7. Full-Length Industry Project: Currency Exchange Adapter

Business Scenario

National Bank of India's international transfer service needs to display exchange rates and convert currencies. The bank's internal system uses INR and city names. The external forex API (ExchangeRateAPI) uses ISO currency codes (USD, EUR, GBP) and returns rates in a different response format. An adapter bridges the gap so the bank's service can call the external API without knowing its internal format.

High-Level Design (HLD)

+---------------------------------------------------+ | BANK SERVICE LAYER | | CurrencyService (uses ICurrencyProvider) | | - getExchangeRate("US Dollar") | | - convert(1000, "INR", "US Dollar") | | (uses currency NAMES, expects INR-based rates) | +----------------------------+----------------------+ | v +---------------------------------------------------+ | ADAPTER LAYER | | ForexAPIAdapter (implements ICurrencyProvider) | | - converts currency NAME to ISO CODE | | - calls external API with ISO code | | - converts response format to bank format | +----------------------------+----------------------+ | v +---------------------------------------------------+ | EXTERNAL API LAYER (cannot modify) | | IForexAPI interface | | ForexAPIImpl | | - getRate(String isoCode): ExternalRate | | (uses ISO codes: USD, EUR, GBP, JPY) | | (returns rate in its own format) | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | ICurrencyProvider (bank's interface) | +-------------------------------------------------------+ | + getExchangeRate(currencyName): ExchangeRate | | + convert(amount, from, to): ConversionResult | | + getSupportedCurrencies(): List<String> | +-------------------------------------------------------+ ^ | +-------------------------------------------------------+ | ForexAPIAdapter (implements ICurrencyProvider) | +-------------------------------------------------------+ | - forexAPI: IForexAPI [HAS-A, composition] | | - nameToCodeMap: Map [conversion lookup] | +-------------------------------------------------------+ | + getExchangeRate(name): ExchangeRate | | + convert(amount, from, to): ConversionResult | | - resolveCode(name): String [name to ISO code] | | - toExchangeRate(ext): ExchangeRate [format convert] | +-------------------------------------------------------+ +-------------------------------------------------------+ | IForexAPI (external, cannot modify) | +-------------------------------------------------------+ | + getRate(isoCode): ExternalRate | +-------------------------------------------------------+ ^ | +-------------------------------------------------------+ | ForexAPIImpl (external implementation) | +-------------------------------------------------------+ | + getRate(isoCode): ExternalRate | | (returns rate relative to USD, in external format) | +-------------------------------------------------------+ +------------------------------+ +------------------------------+ | ExternalRate (external model)| | ExchangeRate (bank model) | +------------------------------+ +------------------------------+ | - isoCode: String | | - currencyName: String | | - rateToUSD: double | | - isoCode: String | | - lastUpdated: long | | - rateToINR: double | +------------------------------+ | - lastUpdated: String | +------------------------------+ +------------------------------+ | ConversionResult | +------------------------------+ | - fromCurrency: String | | - toCurrency: String | | - originalAmount: double | | - convertedAmount: double | | - rate: double | +------------------------------+

Project Structure

nbi-currency-adapter/ ├── src/ │ └── main/java/com/nbi/currency/ │ ├── external/ │ │ ├── IForexAPI.java (external interface, cannot modify) │ │ ├── ForexAPIImpl.java (external implementation) │ │ └── ExternalRate.java (external model) │ ├── model/ │ │ ├── ExchangeRate.java (bank's model) │ │ └── ConversionResult.java │ ├── adapter/ │ │ └── ForexAPIAdapter.java (adapter) │ ├── provider/ │ │ └── ICurrencyProvider.java (bank's interface) │ ├── service/ │ │ └── CurrencyService.java │ └── Application.java ├── src/ │ └── test/java/com/nbi/currency/ │ └── adapter/ │ └── ForexAPIAdapterTest.java └── pom.xml

External API (third-party, cannot modify)

// ExternalRate.java (external model)
package com.nbi.currency.external;

public class ExternalRate {
    private final String isoCode;
    private final double rateToUSD;
    private final long lastUpdatedEpoch;

    public ExternalRate(String isoCode, double rateToUSD, long lastUpdatedEpoch) {
        this.isoCode = isoCode;
        this.rateToUSD = rateToUSD;
        this.lastUpdatedEpoch = lastUpdatedEpoch;
    }

    public String getIsoCode() { return isoCode; }
    public double getRateToUSD() { return rateToUSD; }
    public long getLastUpdatedEpoch() { return lastUpdatedEpoch; }
}

// IForexAPI.java (external interface)
package com.nbi.currency.external;

public interface IForexAPI {
    ExternalRate getRate(String isoCode);
}

// ForexAPIImpl.java (external implementation)
package com.nbi.currency.external;

public class ForexAPIImpl implements IForexAPI {

    @Override
    public ExternalRate getRate(String isoCode) {
        // Simulated rates (1 unit of currency = X USD)
        long now = System.currentTimeMillis();
        switch (isoCode.toUpperCase()) {
            case "INR": return new ExternalRate("INR", 0.012, now);
            case "USD": return new ExternalRate("USD", 1.0, now);
            case "EUR": return new ExternalRate("EUR", 1.08, now);
            case "GBP": return new ExternalRate("GBP", 1.26, now);
            case "JPY": return new ExternalRate("JPY", 0.0067, now);
            case "AED": return new ExternalRate("AED", 0.27, now);
            default:
                throw new IllegalArgumentException("Unsupported currency: " + isoCode);
        }
    }
}

Bank's model and interface

// ExchangeRate.java (bank's model)
package com.nbi.currency.model;

public class ExchangeRate {
    private final String currencyName;
    private final String isoCode;
    private final double rateToINR;
    private final String lastUpdated;

    public ExchangeRate(String currencyName, String isoCode,
                        double rateToINR, String lastUpdated) {
        this.currencyName = currencyName;
        this.isoCode = isoCode;
        this.rateToINR = rateToINR;
        this.lastUpdated = lastUpdated;
    }

    public String getCurrencyName() { return currencyName; }
    public String getIsoCode() { return isoCode; }
    public double getRateToINR() { return rateToINR; }
    public String getLastUpdated() { return lastUpdated; }

    @Override
    public String toString() {
        return String.format("1 %s (%s) = Rs %.2f INR [updated: %s]",
            currencyName, isoCode, rateToINR, lastUpdated);
    }
}

// ConversionResult.java
package com.nbi.currency.model;

public class ConversionResult {
    private final String fromCurrency;
    private final String toCurrency;
    private final double originalAmount;
    private final double convertedAmount;
    private final double rate;

    public ConversionResult(String fromCurrency, String toCurrency,
                            double originalAmount, double convertedAmount, double rate) {
        this.fromCurrency = fromCurrency;
        this.toCurrency = toCurrency;
        this.originalAmount = originalAmount;
        this.convertedAmount = convertedAmount;
        this.rate = rate;
    }

    public double getConvertedAmount() { return convertedAmount; }

    @Override
    public String toString() {
        return String.format("%.2f %s = %.2f %s (rate: %.4f)",
            originalAmount, fromCurrency, convertedAmount, toCurrency, rate);
    }
}

// ICurrencyProvider.java (bank's interface)
package com.nbi.currency.provider;

import com.nbi.currency.model.ExchangeRate;
import com.nbi.currency.model.ConversionResult;
import java.util.List;

public interface ICurrencyProvider {
    ExchangeRate getExchangeRate(String currencyName);
    ConversionResult convert(double amount, String fromCurrency, String toCurrency);
    List<String> getSupportedCurrencies();
}

ForexAPIAdapter.java (the adapter)

package com.nbi.currency.adapter;

import com.nbi.currency.external.ExternalRate;
import com.nbi.currency.external.ForexAPIImpl;
import com.nbi.currency.external.IForexAPI;
import com.nbi.currency.model.ConversionResult;
import com.nbi.currency.model.ExchangeRate;
import com.nbi.currency.provider.ICurrencyProvider;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;

public class ForexAPIAdapter implements ICurrencyProvider {
    private final IForexAPI forexAPI;  // HAS-A (composition with external component)

    private static final Map<String, String> NAME_TO_CODE = new LinkedHashMap<>();
    static {
        NAME_TO_CODE.put("Indian Rupee", "INR");
        NAME_TO_CODE.put("US Dollar", "USD");
        NAME_TO_CODE.put("Euro", "EUR");
        NAME_TO_CODE.put("British Pound", "GBP");
        NAME_TO_CODE.put("Japanese Yen", "JPY");
        NAME_TO_CODE.put("UAE Dirham", "AED");
    }

    private static final DateTimeFormatter FORMATTER =
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
            .withZone(ZoneId.of("Asia/Kolkata"));

    public ForexAPIAdapter() {
        this.forexAPI = new ForexAPIImpl();
    }

    // Constructor for testing with mock API
    public ForexAPIAdapter(IForexAPI forexAPI) {
        this.forexAPI = forexAPI;
    }

    @Override
    public ExchangeRate getExchangeRate(String currencyName) {
        // Conversion 1: currency name to ISO code
        String isoCode = resolveCode(currencyName);

        // Call external API with ISO code
        ExternalRate externalRate = forexAPI.getRate(isoCode);

        // Conversion 2: USD-based rate to INR-based rate
        ExternalRate inrRate = forexAPI.getRate("INR");
        double rateToINR = externalRate.getRateToUSD() / inrRate.getRateToUSD();

        // Conversion 3: epoch timestamp to readable date
        String lastUpdated = FORMATTER.format(
            Instant.ofEpochMilli(externalRate.getLastUpdatedEpoch()));

        return new ExchangeRate(currencyName, isoCode, rateToINR, lastUpdated);
    }

    @Override
    public ConversionResult convert(double amount, String fromCurrency, String toCurrency) {
        ExchangeRate fromRate = getExchangeRate(fromCurrency);
        ExchangeRate toRate = getExchangeRate(toCurrency);

        // Convert: from -> INR -> to
        double amountInINR = amount * fromRate.getRateToINR();
        double convertedAmount = amountInINR / toRate.getRateToINR();
        double directRate = fromRate.getRateToINR() / toRate.getRateToINR();

        return new ConversionResult(fromCurrency, toCurrency,
            amount, convertedAmount, directRate);
    }

    @Override
    public List<String> getSupportedCurrencies() {
        return new ArrayList<>(NAME_TO_CODE.keySet());
    }

    private String resolveCode(String currencyName) {
        String code = NAME_TO_CODE.get(currencyName);
        if (code == null) {
            throw new IllegalArgumentException(
                "Unknown currency: " + currencyName
                + ". Supported: " + NAME_TO_CODE.keySet());
        }
        return code;
    }
}

CurrencyService.java

package com.nbi.currency.service;

import com.nbi.currency.adapter.ForexAPIAdapter;
import com.nbi.currency.model.ConversionResult;
import com.nbi.currency.model.ExchangeRate;
import com.nbi.currency.provider.ICurrencyProvider;

public class CurrencyService {
    private final ICurrencyProvider provider;

    public CurrencyService() {
        this.provider = new ForexAPIAdapter();
    }

    // Constructor for testing with mock provider
    public CurrencyService(ICurrencyProvider provider) {
        this.provider = provider;
    }

    public void displayRate(String currencyName) {
        ExchangeRate rate = provider.getExchangeRate(currencyName);
        System.out.println("  " + rate);
    }

    public ConversionResult convertCurrency(double amount,
                                             String from, String to) {
        ConversionResult result = provider.convert(amount, from, to);
        System.out.println("  " + result);
        return result;
    }
}

Application.java

package com.nbi.currency;

import com.nbi.currency.service.CurrencyService;

public class Application {
    public static void main(String[] args) {
        CurrencyService service = new CurrencyService();

        // Scenario 1: Display exchange rates
        System.out.println("=== Scenario 1: Exchange Rates ===");
        service.displayRate("US Dollar");
        service.displayRate("Euro");
        service.displayRate("British Pound");
        service.displayRate("Japanese Yen");
        service.displayRate("UAE Dirham");
        System.out.println();

        // Scenario 2: Convert INR to USD
        System.out.println("=== Scenario 2: INR to USD ===");
        service.convertCurrency(100000, "Indian Rupee", "US Dollar");
        System.out.println();

        // Scenario 3: Convert USD to EUR
        System.out.println("=== Scenario 3: USD to EUR ===");
        service.convertCurrency(5000, "US Dollar", "Euro");
        System.out.println();

        // Scenario 4: Convert GBP to INR
        System.out.println("=== Scenario 4: GBP to INR ===");
        service.convertCurrency(1000, "British Pound", "Indian Rupee");
        System.out.println();

        // Scenario 5: Invalid currency
        System.out.println("=== Scenario 5: Invalid Currency ===");
        try {
            service.displayRate("Bitcoin");
        } catch (IllegalArgumentException e) {
            System.out.println("  Error: " + e.getMessage());
        }
        System.out.println();

        // Scenario 6: List supported currencies
        System.out.println("=== Scenario 6: Supported Currencies ===");
        var adapter = new com.nbi.currency.adapter.ForexAPIAdapter();
        System.out.println("  " + adapter.getSupportedCurrencies());
    }
}

Expected Output

=== Scenario 1: Exchange Rates === 1 US Dollar (USD) = Rs 83.33 INR [updated: 2026-11-02 18:00:00] 1 Euro (EUR) = Rs 90.00 INR [updated: 2026-11-02 18:00:00] 1 British Pound (GBP) = Rs 105.00 INR [updated: 2026-11-02 18:00:00] 1 Japanese Yen (JPY) = Rs 0.56 INR [updated: 2026-11-02 18:00:00] 1 UAE Dirham (AED) = Rs 22.50 INR [updated: 2026-11-02 18:00:00] === Scenario 2: INR to USD === 100000.00 Indian Rupee = 1200.00 US Dollar (rate: 0.0120) === Scenario 3: USD to EUR === 5000.00 US Dollar = 4629.63 Euro (rate: 0.9259) === Scenario 4: GBP to INR === 1000.00 British Pound = 105000.00 Indian Rupee (rate: 105.0000) === Scenario 5: Invalid Currency === Error: Unknown currency: Bitcoin. Supported: [Indian Rupee, US Dollar, Euro, British Pound, Japanese Yen, UAE Dirham] === Scenario 6: Supported Currencies === [Indian Rupee, US Dollar, Euro, British Pound, Japanese Yen, UAE Dirham]

ForexAPIAdapterTest.java

package com.nbi.currency.adapter;

import com.nbi.currency.external.ExternalRate;
import com.nbi.currency.external.IForexAPI;
import com.nbi.currency.model.ConversionResult;
import com.nbi.currency.model.ExchangeRate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class ForexAPIAdapterTest {
    private ForexAPIAdapter adapter;

    @BeforeEach
    void setUp() {
        // Mock external API with fixed rates for predictable testing
        IForexAPI mockAPI = (isoCode) -> {
            switch (isoCode) {
                case "INR": return new ExternalRate("INR", 0.012, System.currentTimeMillis());
                case "USD": return new ExternalRate("USD", 1.0, System.currentTimeMillis());
                case "EUR": return new ExternalRate("EUR", 1.08, System.currentTimeMillis());
                case "GBP": return new ExternalRate("GBP", 1.26, System.currentTimeMillis());
                default: throw new IllegalArgumentException("Unknown: " + isoCode);
            }
        };
        adapter = new ForexAPIAdapter(mockAPI);
    }

    @Test
    void getExchangeRate_convertsNameToCode() {
        ExchangeRate rate = adapter.getExchangeRate("US Dollar");
        assertEquals("USD", rate.getIsoCode());
    }

    @Test
    void getExchangeRate_convertsToINR() {
        ExchangeRate rate = adapter.getExchangeRate("US Dollar");
        assertTrue(rate.getRateToINR() > 0);
        // 1 USD = 1.0/0.012 = ~83.33 INR
        assertEquals(83.33, rate.getRateToINR(), 1.0);
    }

    @Test
    void getExchangeRate_formatsDate() {
        ExchangeRate rate = adapter.getExchangeRate("Euro");
        assertNotNull(rate.getLastUpdated());
        assertTrue(rate.getLastUpdated().contains("2026"));
    }

    @Test
    void convert_INRtoUSD() {
        ConversionResult result = adapter.convert(83333, "Indian Rupee", "US Dollar");
        assertEquals(1000.0, result.getConvertedAmount(), 5.0);
    }

    @Test
    void convert_USDtoEUR() {
        ConversionResult result = adapter.convert(1000, "US Dollar", "Euro");
        assertTrue(result.getConvertedAmount() > 0);
    }

    @Test
    void getExchangeRate_throwsForUnknown() {
        assertThrows(IllegalArgumentException.class,
            () -> adapter.getExchangeRate("Bitcoin"));
    }

    @Test
    void getSupportedCurrencies_returnsAll() {
        var currencies = adapter.getSupportedCurrencies();
        assertTrue(currencies.contains("US Dollar"));
        assertTrue(currencies.contains("Indian Rupee"));
        assertTrue(currencies.contains("Euro"));
    }

    @Test
    void adapter_usesComposition() {
        // Adapter should work with any IForexAPI implementation
        // (mock or real). This proves Object Adapter pattern.
        assertNotNull(adapter.getExchangeRate("British Pound"));
    }
}

Key observations from the project

  1. Three conversions performed by the adapter: Currency name to ISO code, USD-based rate to INR-based rate, epoch timestamp to human-readable date. The bank service never knows about ISO codes or USD base rates.
  2. Object Adapter pattern: ForexAPIAdapter holds IForexAPI reference (HAS-A composition). This allows injecting a mock API for testing.
  3. External API is untouched: IForexAPI and ForexAPIImpl are not modified. The adapter wraps them without changing their code.
  4. CurrencyService uses only the bank's interface: It depends on ICurrencyProvider, not on the adapter directly. Tomorrow, if the external API changes, only the adapter changes.
  5. Testable with mocks: The test creates a mock IForexAPI with fixed rates, injects it into the adapter via constructor, and verifies conversions with predictable values.

8. When to Use and When NOT to Use

When to use Adapter

ScenarioExampleWhy Adapter
Integrating third-party APIsWeather API, Payment Gateway, SMS serviceExternal API has a different interface than your system expects
Legacy system integrationOld SOAP service with new REST clientConvert between old and new data formats
Data format conversionXML to JSON, CSV to database recordsSource and destination use different formats
Unit conversionsFahrenheit to Celsius, miles to kmExternal system uses different units
Wrapping vendor librariesSwitching from one logging lib to anotherAdapter isolates your code from vendor-specific APIs

When NOT to use Adapter

ScenarioWhy NOTUse Instead
Interfaces are already compatibleNo conversion neededDirect usage
You control both sidesModify the source interface insteadRefactor the interface
Too many conversionsAdapter becomes a god class doing everythingBreak into multiple focused adapters

9. Quick Reference Card

ComponentRoleIn Currency Project
Target InterfaceThe interface your client expectsICurrencyProvider
AdapteeThe existing class with incompatible interfaceIForexAPI + ForexAPIImpl
AdapterConverts between target and adapteeForexAPIAdapter
ClientUses the target interfaceCurrencyService