Adapter & Facade Patterns
Two of the most practically useful structural patterns are the Adapter and the Facade. Both sit between your code and something you do not fully control — a third-party library, a legacy system, or a complex subsystem — but they solve opposite problems: the Adapter makes an incompatible interface compatible, while the Facade makes a complex interface simple.
The Adapter Pattern
GoF intent: "Convert the interface of a class into another interface that clients expect. Adapter lets classes work together that could not otherwise because of incompatible interfaces."
Think of a physical power-plug adapter: the socket has one shape, your device has another, the adapter sits in between and speaks both languages without changing either side.
When to reach for it
- You are integrating a third-party or legacy class whose interface you cannot (or should not) change.
- You want to use several unrelated classes through a single interface (e.g. multiple payment gateways behind one
PaymentGateway interface).
- You want to decouple client code from a specific implementation so you can swap it later.
Class Adapter vs. Object Adapter
Java supports both forms. The class adapter extends the adaptee (possible only when there is no other superclass needed). The object adapter — far more common — wraps the adaptee via composition, which is generally preferred because it does not lock you into one concrete class and works with subclasses of the adaptee too.
Favour composition over inheritance. The object adapter is almost always the right choice in Java. Class adapters are a design smell unless you have a compelling reason.
Realistic example: plugging a legacy XML report service into a JSON-based pipeline
Suppose your application works with a ReportExporter interface that expects JSON output. You have a third-party library that only produces XML. You cannot modify the library.
// Target interface — what your application speaks
public interface ReportExporter {
String exportAsJson(String reportId);
}
// Adaptee — the legacy XML library you cannot change
public class LegacyXmlReportService {
public String generateXmlReport(String id) {
// Imagine real XML generation here
return "<report><id>" + id + "</id></report>";
}
public String getMetadataXml(String id) {
return "<meta><generated>true</generated></meta>";
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
// Object Adapter — wraps the adaptee, implements the target interface
public class XmlToJsonReportAdapter implements ReportExporter {
private final LegacyXmlReportService xmlService;
private final XmlMapper xmlMapper = new XmlMapper();
private final ObjectMapper jsonMapper = new ObjectMapper();
public XmlToJsonReportAdapter(LegacyXmlReportService xmlService) {
this.xmlService = xmlService;
}
@Override
public String exportAsJson(String reportId) {
try {
String xml = xmlService.generateXmlReport(reportId);
// Parse XML into a generic map, then serialise as JSON
var node = xmlMapper.readTree(xml.getBytes());
return jsonMapper.writeValueAsString(node);
} catch (Exception e) {
throw new ReportConversionException("Failed to adapt XML report to JSON", e);
}
}
}
// Client code — knows only about ReportExporter, never about XML
public class ReportingService {
private final ReportExporter exporter;
public ReportingService(ReportExporter exporter) {
this.exporter = exporter;
}
public void publishReport(String reportId) {
String json = exporter.exportAsJson(reportId);
// send json to downstream pipeline...
System.out.println("Publishing: " + json);
}
}
// Wiring (e.g. in a factory or DI configuration)
var legacy = new LegacyXmlReportService();
ReportExporter adapter = new XmlToJsonReportAdapter(legacy);
var service = new ReportingService(adapter);
service.publishReport("Q1-2025");
The client never sees LegacyXmlReportService. Tomorrow you can swap in a native JSON provider by writing a different ReportExporter implementation — the client is unchanged.
Testing adapters: An adapter is a thin translation layer, so unit tests are straightforward — stub the adaptee, invoke the adapter method, and assert the translated output. Do not let business logic creep into adapters; keep them focused solely on interface translation.
The Facade Pattern
GoF intent: "Provide a simplified interface to a complex body of code."
A Facade does not adapt one interface to another — it hides a whole subsystem behind a single, cohesive entry point. The subsystem classes still exist and can still be used directly by code that needs fine-grained control, but most callers just use the facade.
When to reach for it
- A subsystem has grown complex and callers have to orchestrate many classes in the right order.
- You want to layer your system (UI calls a facade, facade coordinates domain/infrastructure layers).
- You want to decouple a subsystem from its clients so the subsystem can evolve without rippling changes outward.
Realistic example: video transcoding pipeline
Encoding a video involves several steps: validating the source file, extracting audio, resizing frames, encoding, generating a thumbnail, and writing metadata. Without a facade, every caller must know and orchestrate all of these classes.
// Subsystem classes — each focused on one concern
public class VideoValidator {
public void validate(String filePath) {
System.out.println("Validating: " + filePath);
}
}
public class AudioExtractor {
public String extract(String filePath) {
System.out.println("Extracting audio from: " + filePath);
return filePath + ".aac";
}
}
public class VideoEncoder {
public String encode(String filePath, String resolution) {
System.out.println("Encoding " + filePath + " at " + resolution);
return filePath + ".mp4";
}
}
public class ThumbnailGenerator {
public String generate(String videoPath) {
System.out.println("Generating thumbnail for: " + videoPath);
return videoPath + ".jpg";
}
}
public class MetadataWriter {
public void write(String videoPath, String audioPath, String thumbnail) {
System.out.println("Writing metadata for: " + videoPath);
}
}
// Facade — orchestrates the subsystem behind a single, simple method
public class VideoTranscodingFacade {
private final VideoValidator validator;
private final AudioExtractor audioExtractor;
private final VideoEncoder encoder;
private final ThumbnailGenerator thumbnailGenerator;
private final MetadataWriter metadataWriter;
// Dependencies injected — the facade does NOT instantiate them itself
public VideoTranscodingFacade(
VideoValidator validator,
AudioExtractor audioExtractor,
VideoEncoder encoder,
ThumbnailGenerator thumbnailGenerator,
MetadataWriter metadataWriter) {
this.validator = validator;
this.audioExtractor = audioExtractor;
this.encoder = encoder;
this.thumbnailGenerator = thumbnailGenerator;
this.metadataWriter = metadataWriter;
}
public String transcode(String sourcePath, String resolution) {
validator.validate(sourcePath);
String audio = audioExtractor.extract(sourcePath);
String encoded = encoder.encode(sourcePath, resolution);
String thumbnail = thumbnailGenerator.generate(encoded);
metadataWriter.write(encoded, audio, thumbnail);
return encoded;
}
}
// Client code — calls one method, stays blissfully ignorant of the subsystem
VideoTranscodingFacade facade = new VideoTranscodingFacade(
new VideoValidator(),
new AudioExtractor(),
new VideoEncoder(),
new ThumbnailGenerator(),
new MetadataWriter()
);
String output = facade.transcode("/uploads/raw-video.mov", "1080p");
System.out.println("Done: " + output);
A facade is not a God object. If you pile business logic, error recovery, caching, and cross-cutting concerns all into one facade class it becomes a maintenance nightmare. Keep the facade as a thin orchestration layer. If it exceeds ~150 lines, split the subsystem into smaller facades.
Adapter vs. Facade — Choosing the Right Pattern
Both patterns introduce an intermediate class, so it is easy to conflate them. The distinction is purpose:
- Adapter: the target interface is fixed (defined by the client); the adaptee interface is fixed (defined by the library); the adapter translates between them. The existing interfaces are what drive its design.
- Facade: no existing interface mismatch. The facade defines a new, simpler interface that hides a complex subsystem that you own or can see inside.
In practice both patterns are often used together: a facade for your internal subsystem, with adapters for each external library the subsystem depends on.
Summary
The Adapter pattern bridges an incompatible interface by wrapping an adaptee object (object adapter) or extending it (class adapter). It lets you integrate third-party or legacy code without touching client code. The Facade pattern defines a simplified interface over a complex subsystem, reducing coupling and cognitive load for callers. Both are invaluable in real-world Java development: use Adapter when you face an interface mismatch, and Facade when you face complexity that should be hidden.