Adapter
Sooner or later, you'll want to use a class that does exactly what you need — except its methods are named wrong, take arguments in the wrong shape, or speak a different data format than the rest of your code expects. The Adapter pattern answers a narrow, practical question: how do you make an existing, incompatible interface fit the interface your code already expects — without rewriting either side?
⚠️ The Problem
Say your checkout code is written against a clean interface: every payment provider you
use exposes a pay(amount) method that takes a dollar amount. Now you need to
integrate a third-party payment library, and its API looks nothing like that — it exposes
makePayment(amountCents), taking an integer number of cents instead of dollars,
and returning a differently-shaped result object. You can't change the third-party library's
source. You also don't want to litter your checkout code with special-case branches for "if
this is the weird provider, convert dollars to cents and call the other method name" —
that logic would leak into every place that processes a payment.
✅ The Solution
Write a small Adapter class that implements the interface your code already expects
(pay(amount)), and internally translates each call into whatever the wrapped
class actually needs (makePayment(amount * 100)). Your checkout code only ever
talks to the pay(amount) interface — it has no idea an adapter, or a mismatched
third-party library, is even involved. The translation logic lives in exactly one place: the
adapter.
Example 1 — adapting an old XML weather API to a new JSON interface
// The interface our app is written against:
// class implementing WeatherSource must expose getForecast(city) -> { tempC, summary }
// The old, incompatible class we can't change — it only speaks XML strings.
class LegacyXmlWeatherApi {
fetchXmlForecast(cityCode) {
// Pretend this hits a real endpoint and returns an XML string.
return `<forecast><city>${cityCode}</city><tempF>71</tempF><desc>Cloudy</desc></forecast>`;
}
}
// The Adapter: implements the interface our app expects, wraps the legacy class internally.
class WeatherApiAdapter {
constructor(legacyApi) {
this.legacyApi = legacyApi;
}
getForecast(city) {
const cityCode = city.toUpperCase().slice(0, 3);
const xml = this.legacyApi.fetchXmlForecast(cityCode);
// Translate: parse the legacy XML shape into the shape our app expects.
const tempF = Number(xml.match(/<tempF>(\d+)<\/tempF>/)[1]);
const summary = xml.match(/<desc>([^<]+)<\/desc>/)[1];
return {
tempC: Math.round((tempF - 32) * (5 / 9)),
summary,
};
}
}
// Client code only ever sees getForecast(city) — never the XML, never the legacy method name.
function printForecast(weatherSource, city) {
const { tempC, summary } = weatherSource.getForecast(city);
console.log(`${city}: ${tempC}°C, ${summary}`);
}
const adapter = new WeatherApiAdapter(new LegacyXmlWeatherApi());
printForecast(adapter, "Chicago"); // Chicago: 22°C, Cloudy
printForecast never knows the underlying data started life as an XML string
produced by a method called fetchXmlForecast. Swap in a real modern JSON weather
API tomorrow, and only the adapter — or a different adapter — needs to change.
Example 2 — adapting a legacy printer driver to a modern interface
// Modern interface the rest of the application is written against.
interface PrinterInterface {
void printDocument(String text);
}
// Legacy driver we can't touch — different method name, different argument order/shape.
class LegacyPrinterDriver {
void sendJob(byte[] rawBytes, boolean duplex) {
System.out.println("[legacy driver] printing " + rawBytes.length + " bytes, duplex=" + duplex);
}
}
// Adapter: implements PrinterInterface, delegates to the legacy driver underneath.
class LegacyPrinterAdapter implements PrinterInterface {
private final LegacyPrinterDriver legacyDriver;
LegacyPrinterAdapter(LegacyPrinterDriver legacyDriver) {
this.legacyDriver = legacyDriver;
}
@Override
public void printDocument(String text) {
byte[] rawBytes = text.getBytes();
legacyDriver.sendJob(rawBytes, false);
}
}
// Client code depends only on PrinterInterface.
class DocumentPrintService {
private final PrinterInterface printer;
DocumentPrintService(PrinterInterface printer) {
this.printer = printer;
}
void print(String text) {
printer.printDocument(text);
}
}
// Usage:
PrinterInterface printer = new LegacyPrinterAdapter(new LegacyPrinterDriver());
new DocumentPrintService(printer).print("Invoice #4471");
Any code depending on PrinterInterface — including brand-new printers that
implement it directly — works interchangeably with the decades-old driver, as long as an
adapter sits between them.
👍 When To Use It
- You need to use an existing class, but its interface doesn't match what the rest of your code expects, and you can't (or shouldn't) modify that class's source.
- You're integrating a third-party library or legacy system and want to isolate its quirks behind a single, clean boundary instead of spreading conversion logic everywhere.
- You want to swap one implementation for another (e.g. old library for new) later without touching client code, as long as each sits behind its own adapter.
👎 When NOT To Use It
- When you control both interfaces — if you can just change one of them to match the other, do that instead of adding an adapter layer for no reason.
- When the "incompatibility" is trivial (e.g. just a renamed parameter) — sometimes a thin wrapper function is enough and a whole class is overkill.
- When you're adapting so many methods that the adapter becomes a second, parallel copy of the wrapped class's entire API — that's often a sign the two systems should be unified instead of perpetually translated.
❓ FAQ
Isn't Adapter just a wrapper with a nicer name?
It is a wrapper — but a specific kind: one whose entire purpose is translating one existing interface into a different existing interface the client expects, with no new behavior added. Other patterns (like Decorator) also wrap objects, but for different reasons — see the FAQ in the Decorator lesson for that distinction.
What's the difference between an Adapter and a Facade?
An Adapter makes one existing interface look like a different single interface a client already expects — it's about translation, one-to-one. A Facade simplifies access to a whole subsystem of multiple classes behind one new, simpler interface — it's about reducing complexity, not matching an existing contract. See the Facade lesson later in this track.
Can an Adapter wrap more than one class?
Yes — nothing stops an adapter from coordinating calls to several underlying objects internally, as long as it still exposes the single interface the client expects. Once that coordination gets complex enough to be the main point, though, you're arguably looking at a Facade instead of a plain Adapter.
🙋 There Are No Dumb Questions
Q: Why not just edit the third-party class directly to match my interface?
A: Usually you can't — it might be a library you don't own, ship as a compiled dependency, or
get overwritten on every update. Even when you technically could edit it, doing so couples
your code to a fork you now have to maintain forever. An adapter keeps the third-party code
untouched and swappable, and keeps the translation logic in one clearly-named place instead
of hidden inside a patched dependency.
✅ Quick Check
What is the core job of an Adapter class?
Next up: what happens when a class hierarchy has to vary along two independent dimensions at once — like shape type and rendering method — and subclassing both would explode into every combination? That's the Bridge pattern.