Early Access: 87 spots left.

Claim

Course content

No content available

The Adapter pattern lets two pieces of code that were never designed to talk to each other work together. You have a class that uses one interface (WeatherProvider, say) and a third-party library that exposes a different one (LegacyWeatherSdk with Fahrenheit and integer codes). Instead of changing the library (which you cannot) or rewriting the application code (which you should not), you slip a small intermediary in between: the Adapter. It implements the interface the application expects, holds a reference to the third-party object, and translates every call.

The shape is mechanical: a new class LegacyWeatherAdapter implements WeatherProvider, holds a LegacyWeatherSdk field, and forwards each method through the field while doing whatever conversion is needed (Fahrenheit to Celsius, integer code to string). The application keeps depending on WeatherProvider and never imports the legacy SDK directly. If the SDK is replaced tomorrow, you write another adapter; nothing in the application changes. The page starts from the coupled version, the one without the intermediary.

The conversion math ((f - 32) * 5/9) and the integer-to-string mapping are not the forecast service's job. They are translation logic from a specific vendor's API into the application's vocabulary. By living inside ForecastService, they tie the service to that one vendor.

Three concrete problems follow. First, every other service that needs a temperature reading copies that conversion line, and the day someone gets the formula wrong is the day half the application is producing bad numbers. Second, replacing the SDK becomes a refactoring expedition: every import of the SDK, every readTempF call, every switch on getCondCode has to be tracked down. Third, testing ForecastService requires mocking LegacyWeatherSdk directly: your test has to know the SDK's method names and unit conventions, which means the SDK has leaked into the test suite.

The core issue is that two unrelated concerns, "forecasting" and "how the SDK speaks", are stuck inside the same class. The adapter pattern's job is to peel them apart.

What this costs in production

The weather vendor renamed readTempF to getTempF in a minor SDK bump. Because three services called the SDK directly, the upgrade broke all three at once, and the Fahrenheit-to-Celsius formula had been copied into each with a subtly different rounding. The team spent a release hunting down every sdk. call site by hand. One was missed and shipped temperatures that were off by a degree for a week.

The weather SDK is one instance of a boundary that has gone missing: third-party vocabulary leaking into application code. The symptoms look like these.

SmellWhat it looks likeWhy an adapter is the answer
Application code imports a third-party class directlyimport com.acme.legacy.LegacyWeatherSdk; class ForecastService { LegacyWeatherSdk sdk = new LegacyWeatherSdk(); ... }Every line that touches the SDK is a line that breaks if the vendor changes; the adapter is the firewall
Conversion logic (units, formats, error codes) duplicated at every call sitedouble c = (sdk.readTempF("Paris") - 32) * 5/9; in three different servicesThe conversion belongs in one place, inside the adapter, not scattered across the codebase
An if/else mapping between two code systems shows up in business logicswitch (sdk.getCondCode(loc)) { case 1: return "sunny"; case 2: return "rainy"; ... }The mapping is a translation, not a business decision; the adapter owns it
Wrapping the third-party class in a subclass to add the methods the app expectsclass WeatherWrapper extends LegacyWeatherSdk { double getC(String l) { ... } }Inheriting from the legacy class chains you to its concrete type and pulls all of its other methods into the public surface; composition (the adapter holding it as a field) is cleaner
Mocking the third-party class directly in tests because the application talks to it@Mock LegacyWeatherSdk sdk; every test that touches forecasting needs this mockIf the application talked to a WeatherProvider interface, the test mocks the interface; the adapter is tested separately, once
Two third-party libraries that do similar jobs but expose different APIsLegacyWeatherSdk and CloudWeatherClient both report weather, both with different methods and unitsEach gets its own adapter to the common WeatherProvider interface; the application can swap them with one line

Three things changed and they are all related.

ForecastService no longer mentions LegacyWeatherSdk. It accepts a WeatherProvider and asks it for temperatures and conditions. The forecasting logic, the one formatted output line, is the only thing left in this class. Anyone reading it can answer "what does forecasting do?" without learning a third-party SDK first.

The translation logic, the Fahrenheit math and the integer-to-string mapping, has moved into LegacyWeatherAdapter and lives there in one place. If the formula is wrong, you fix it once. If the vendor renames readTempF to getFahrenheit next quarter, you change the adapter and ship; nothing else moves. The conversion is no longer a per-call-site concern.

The whole stack composes. ForecastService works with any WeatherProvider; LegacyWeatherAdapter is one such provider. Tomorrow you add CloudWeatherAdapter for a SaaS API, MockWeatherProvider for tests, and CachedWeatherProvider (a decorator on top of any provider) for performance, and the forecast service is untouched. The interface is the contract; everything else plugs in around it. Tests of ForecastService mock WeatherProvider, not LegacyWeatherSdk; the SDK never leaks into the test classpath.

depends onimplementswraps + convertsForecastServiceWeatherProviderLegacyAdapterLegacyWeatherSdk
ForecastService depends only on the WeatherProvider interface. LegacyWeatherAdapter implements that interface and wraps the third-party LegacyWeatherSdk, converting Fahrenheit and condition codes in one place. Swapping the vendor means writing another adapter, not editing ForecastService.

With the refactor in hand, it is worth separating Adapter from the patterns it is commonly confused with, because all of them wrap one object inside another. The differences are about intent, not shape.

Adapter vs. Decorator: an Adapter changes an object's interface; the wrapped object is talked to differently from the outside than from the inside. A Decorator keeps the same interface and adds behaviour: BufferedInputStream wraps an InputStream and is itself an InputStream, just with buffering layered on. Both compose, but Adapter targets API mismatch while Decorator targets behavioural augmentation.

Adapter vs. Facade: a Facade hides a subsystem of many classes behind one simpler interface; think of MockMvc masking the entire Spring MVC test rig. An Adapter wraps one object and exposes it through a different interface. Facade is about reducing surface area; Adapter is about translating a surface that already has the right shape but the wrong vocabulary.

Adapter vs. Bridge: Bridge is what you reach for up front when you know two dimensions of variation will need to evolve independently. The abstraction (Shape) and the implementation (Renderer) get separate hierarchies on day one and you compose them. Adapter is what you reach for retroactively to make an existing class fit a new contract. Bridge is a design choice; Adapter is a fix.

The rule of thumb: if you wrote an interface and the wrapped class does not implement it (because someone else wrote that class), you want Adapter. If the wrapped class already implements the interface and you want to add behaviour, you want Decorator.

Arrays.asList(array) is the canonical Adapter in the JDK: it returns a List<T> backed by the original array, no copying, reads and writes going straight through to the array slots. Collections.list(Enumeration) and Collections.enumeration(Collection) adapt between the pre-1.2 Enumeration world and modern collections. In I/O, InputStreamReader is the textbook example: it wraps an InputStream (a byte source) and exposes it as a Reader (a character source), with a charset doing the translation. Its neighbour BufferedReader is the contrast worth memorising: it wraps a Reader and stays a Reader, which makes it a Decorator, not an Adapter.

At framework scale, Spring MVC's HandlerAdapter lets the DispatcherServlet invoke handlers written against completely different conventions (@Controller methods, the old Controller interface, HttpRequestHandler) through one uniform call. SLF4J's bindings are adapters: application code logs against the SLF4J Logger, and whichever binding sits on the classpath translates those calls into Log4j, Logback, or java.util.logging. JDBC drivers adapt each database's wire protocol to the standard Connection, Statement, and ResultSet interfaces, which is why switching databases does not mean rewriting the data access layer.

Python's standard library has the same textbook case: io.TextIOWrapper wraps a binary stream and exposes a text-stream interface, the same role InputStreamReader plays in Java (calling open() in text mode builds one for you). The requests library names the pattern outright: transport adapters like HTTPAdapter translate the session's uniform interface into a specific HTTP implementation, and session.mount("https://", adapter) installs one per URL prefix. Duck typing changes the mechanics but not the need. There is no interface keyword to satisfy, yet you still write the small wrapper class so your code can keep talking in its own vocabulary to a library that speaks another.

C++ puts the word in the standard itself: std::stack, std::queue, and std::priority_queue are container adaptors, thin wrappers that expose a deque or vector through a deliberately different, narrower interface. std::back_insert_iterator adapts a container into the output-iterator shape that algorithms like std::copy expect. The heuristic carries across all three languages: if the wrapper changes the vocabulary of the thing inside, it is an adapter.

ProsCons
Application code never imports the third-party class directly; vendor swap is a one-line changeAdds a class for what was a method call; small overhead per call site
Conversion / translation logic lives in one place (the adapter), not scattered across servicesIf the third-party API and the application interface drift apart, the adapter accumulates more and more translation logic until it becomes its own beast
Tests of application code mock the interface, not the third-party class; the SDK never leaks into the test classpathTwo adapters wrapping the same library can subtly disagree on edge cases (unknown codes, null inputs); centralising the adapter in one module is a discipline issue
Composition (object adapter): you can wrap any subclass of the legacy class, and you can hold it behind an interface for testing the adapter itselfObject adapter cannot override the wrapped class's methods directly, only translate calls; if you need to change a legacy method's behaviour, Adapter is the wrong pattern
Multiple adapters can implement the same interface, so the same application code works with multiple back-ends (legacy SDK, SaaS client, mocks)Stack traces grow one frame per adapter call; debugging through several adapter layers can be tedious

Adapter pays off when the wrapped class has the wrong shape. If it has the right shape and you just want to add behaviour, you want Decorator; wrapping for the sake of wrapping is overhead.

The other failure mode is using Adapter as a permanent crutch around an API you control. If the legacy class is yours and the mismatch is yours to fix, the right answer is usually to evolve the original class (or extract a new interface from it) rather than to wrap it forever. Adapters that wrap an in-house class for years are a sign the team chose convenience over cleanup; the cleanup eventually comes due as the adapter accumulates more and more translation logic.

Finally, Adapter does not help when the impedance mismatch is semantic, not syntactic. If WeatherProvider.getTemperatureCelsius(city) is supposed to return today's temperature and the legacy SDK only knows historical averages, no amount of unit conversion will make it correct. Adapter translates calls; it does not paper over different meaning. If the meanings are different, no wrapper saves you; that is a domain modelling problem, not a structural one.

Three flavours of question recur. The first is design: "design an integration with three different payment providers" or "add support for both Twilio and Plivo as SMS senders". The expected first move is to define the application's interface (PaymentProvider, SmsSender) in the application's vocabulary, then write one adapter per third party. The follow-up tends to be about adding a fourth provider; that is your cue to point out that adapters compose and the application code never moves.

The second is comparison: "Adapter vs. Decorator", "Adapter vs. Facade", "Adapter vs. Bridge". The most useful framing is intent: Adapter changes an object's interface to fit a target the wrapped class does not implement; Decorator keeps the interface and adds behaviour; Facade hides a whole subsystem of many classes behind one new interface; Bridge is a day-one design that separates abstraction and implementation hierarchies. If asked for a concrete distinction, say InputStreamReader is an Adapter (byte stream to char stream), BufferedReader is a Decorator (Reader to Reader, plus buffering), MockMvc is a Facade (whole MVC test rig behind one builder), and a Shape/Renderer split is a Bridge.

The third is implementation depth: "object adapter vs. class adapter". The class-adapter variant uses inheritance: the adapter extends the legacy class and implements the target interface. Java cannot inherit from two classes, so class adapters are largely a curiosity there, and the object-adapter form (composition) is what every Java codebase uses. C++ can genuinely build class adapters through multiple (often private) inheritance, and Python's duck typing mostly dissolves the question. Knowing which variants survive contact with each language's type system is exactly the judgement interviewers are listening for.

The companion exercise gives you a LegacyWeatherSdk (Fahrenheit + integer condition codes) and asks you to write a LegacyWeatherAdapter that implements WeatherProvider. The validator checks that the adapter implements the interface, holds the legacy SDK by composition, and translates Fahrenheit to Celsius and integer codes to strings correctly, including the unknown-code fallback. A ForecastService wired with the adapter must produce the right forecast string end-to-end.

Back to Low Level Design