Adapter Design Pattern
1. What is Adapter Pattern?
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:
- Performing data conversions as required for the destination (e.g., city name to city code, Fahrenheit to Celsius)
- Interacting with the destination component by giving it the necessary inputs in the format it expects
Real-world examples
2. The 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.
3. The Solution: Adapter as Bridge
Place an Adapter class between your local component and the external component. The adapter:
- Implements your local interface (so your app can call it normally)
- Internally creates/holds an instance of the external component
- Converts input data from your format to the external format (city name to city code)
- Calls the external component with the converted data
- Converts the output data from external format to your format (Fahrenheit to Celsius)
- Returns the converted result to your app
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);
}
}
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);
}
}
| Aspect | Object Adapter | Class Adapter |
|---|---|---|
| Mechanism | Composition (HAS-A) | Inheritance (IS-A) |
| Flexibility | Can adapt any subclass of adaptee | Locked to one specific adaptee class |
| Java support | Fully supported | Limited (no multiple class inheritance) |
| Recommended | Yes | Use only when composition is not possible |
6. Pre-defined Adapter Classes in Java
| Adapter | Converts From | Converts To |
|---|---|---|
java.util.Arrays.asList() | Array | List (gives List from given array content) |
java.util.Collections.list() | Enumeration | ArrayList (gives collection from the given content) |
java.util.Collections.enumeration() | Collection | Enumeration (appears in Enumerations) |
java.io.InputStreamReader | InputStream (byte stream) | Reader (character stream) |
java.io.OutputStreamWriter | OutputStream (byte stream) | Writer (character stream) |
javax.xml.bind.annotation.adapters.XmlAdapter | XML 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
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)
Low-Level Design (LLD)
Project Structure
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
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
- 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.
- Object Adapter pattern: ForexAPIAdapter holds
IForexAPIreference (HAS-A composition). This allows injecting a mock API for testing. - External API is untouched:
IForexAPIandForexAPIImplare not modified. The adapter wraps them without changing their code. - 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. - Testable with mocks: The test creates a mock
IForexAPIwith 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
| Scenario | Example | Why Adapter |
|---|---|---|
| Integrating third-party APIs | Weather API, Payment Gateway, SMS service | External API has a different interface than your system expects |
| Legacy system integration | Old SOAP service with new REST client | Convert between old and new data formats |
| Data format conversion | XML to JSON, CSV to database records | Source and destination use different formats |
| Unit conversions | Fahrenheit to Celsius, miles to km | External system uses different units |
| Wrapping vendor libraries | Switching from one logging lib to another | Adapter isolates your code from vendor-specific APIs |
When NOT to use Adapter
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Interfaces are already compatible | No conversion needed | Direct usage |
| You control both sides | Modify the source interface instead | Refactor the interface |
| Too many conversions | Adapter becomes a god class doing everything | Break into multiple focused adapters |
9. Quick Reference Card
| Component | Role | In Currency Project |
|---|---|---|
| Target Interface | The interface your client expects | ICurrencyProvider |
| Adaptee | The existing class with incompatible interface | IForexAPI + ForexAPIImpl |
| Adapter | Converts between target and adaptee | ForexAPIAdapter |
| Client | Uses the target interface | CurrencyService |
Adapter Design Pattern in Java
1. What is Adapter Pattern?
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:
- Performing data conversions as required for the destination (e.g., city name to city code, Fahrenheit to Celsius)
- Interacting with the destination component by giving it the necessary inputs in the format it expects
Real-world examples
2. The 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.
3. The Solution: Adapter as Bridge
Place an Adapter class between your local component and the external component. The adapter:
- Implements your local interface (so your app can call it normally)
- Internally creates/holds an instance of the external component
- Converts input data from your format to the external format (city name to city code)
- Calls the external component with the converted data
- Converts the output data from external format to your format (Fahrenheit to Celsius)
- Returns the converted result to your app
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);
}
}
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);
}
}
| Aspect | Object Adapter | Class Adapter |
|---|---|---|
| Mechanism | Composition (HAS-A) | Inheritance (IS-A) |
| Flexibility | Can adapt any subclass of adaptee | Locked to one specific adaptee class |
| Java support | Fully supported | Limited (no multiple class inheritance) |
| Recommended | Yes | Use only when composition is not possible |
6. Pre-defined Adapter Classes in Java
| Adapter | Converts From | Converts To |
|---|---|---|
java.util.Arrays.asList() | Array | List (gives List from given array content) |
java.util.Collections.list() | Enumeration | ArrayList (gives collection from the given content) |
java.util.Collections.enumeration() | Collection | Enumeration (appears in Enumerations) |
java.io.InputStreamReader | InputStream (byte stream) | Reader (character stream) |
java.io.OutputStreamWriter | OutputStream (byte stream) | Writer (character stream) |
javax.xml.bind.annotation.adapters.XmlAdapter | XML 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
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)
Low-Level Design (LLD)
Project Structure
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
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
- 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.
- Object Adapter pattern: ForexAPIAdapter holds
IForexAPIreference (HAS-A composition). This allows injecting a mock API for testing. - External API is untouched:
IForexAPIandForexAPIImplare not modified. The adapter wraps them without changing their code. - 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. - Testable with mocks: The test creates a mock
IForexAPIwith 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
| Scenario | Example | Why Adapter |
|---|---|---|
| Integrating third-party APIs | Weather API, Payment Gateway, SMS service | External API has a different interface than your system expects |
| Legacy system integration | Old SOAP service with new REST client | Convert between old and new data formats |
| Data format conversion | XML to JSON, CSV to database records | Source and destination use different formats |
| Unit conversions | Fahrenheit to Celsius, miles to km | External system uses different units |
| Wrapping vendor libraries | Switching from one logging lib to another | Adapter isolates your code from vendor-specific APIs |
When NOT to use Adapter
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Interfaces are already compatible | No conversion needed | Direct usage |
| You control both sides | Modify the source interface instead | Refactor the interface |
| Too many conversions | Adapter becomes a god class doing everything | Break into multiple focused adapters |
9. Quick Reference Card
| Component | Role | In Currency Project |
|---|---|---|
| Target Interface | The interface your client expects | ICurrencyProvider |
| Adaptee | The existing class with incompatible interface | IForexAPI + ForexAPIImpl |
| Adapter | Converts between target and adaptee | ForexAPIAdapter |
| Client | Uses the target interface | CurrencyService |