Adapter's purpose and use cases, Adapter vs Decorator, a Java example (legacy logging, third-party SDKs), class vs object adapters in Java, why adapters are an anti-corruption layer for third-party libraries, and Bridge — decoupling abstraction from implementation, Bridge vs Adapter, implementation, scenarios and benefits.
Published September 25, 2026
Adapter and Bridge have similar structures (a wrapper holding a reference) but opposite intents. Adapter fixes a mismatch after the fact. Bridge is designed up front, to let two dimensions vary independently. Making that distinction crisply is what interviewers look for.
Short answer: An adapter converts the interface of an existing class into the interface a client expects, so incompatible components can work together without changing either one. Typical uses:
Example: your checkout depends on a PaymentGateway interface. A new provider's SDK has a different API, so an adapter implements PaymentGateway by calling the SDK.
Learn it in depth → Adapter & Facade
Short answer: It has four participants:
The translation may cover method names, parameter types, units and currencies, error models (mapping SDK exceptions to your own), and synchronous vs asynchronous styles.
Short answer: Use it when you can't or shouldn't change either side: third-party or legacy code, or generated clients. And you want your domain code to depend on your own abstraction. It's the core building block of an anti-corruption layer in domain-driven design, which stops foreign models from leaking into your core.
public interface PaymentGateway { ChargeResult charge(Money amount, String customerToken); }
public final class AcmePayAdapter implements PaymentGateway { // object adapter
private final AcmePayClient sdk; // adaptee (third-party)
public AcmePayAdapter(AcmePayClient sdk) { this.sdk = sdk; }
@Override public ChargeResult charge(Money amount, String token) {
try {
AcmeResponse r = sdk.createCharge(amount.minorUnits(), amount.currency().getCurrencyCode(), token);
return r.isSuccess() ? ChargeResult.approved(r.getId()) : ChargeResult.declined(r.getDeclineCode());
} catch (AcmeTimeoutException e) {
throw new PaymentUnavailableException("AcmePay timeout", e); // translate the error model too
}
}
}
Short answer: An Adapter changes the interface, making X look like Y, while keeping the behaviour. A Decorator keeps the same interface, and adds behaviour (logging, caching, retries, metrics). Decorators can be stacked transparently. Adapters sit at the boundary between two different interfaces.
| Adapter | Decorator | |
|---|---|---|
| Interface | Converts to a different one | The same as the wrapped object |
| Goal | Compatibility | Extra responsibilities |
| Stacking | Rarely | Commonly (new Buffered(new Gzip(new File…)))) |
Short answer: SLF4J bridges are adapters: jul-to-slf4j and log4j-over-slf4j let code written for one logging API emit through SLF4J and Logback. In the JDK, Arrays.asList adapts an array to the List interface, and InputStreamReader adapts a byte stream (InputStream) to a character stream (Reader).
// Adapting a legacy logger to your Logger abstraction
public final class LegacyLoggerAdapter implements AppLogger {
private final OldLogUtil legacy;
public LegacyLoggerAdapter(OldLogUtil legacy) { this.legacy = legacy; }
@Override public void info(String msg, Object... args) { legacy.write(1, MessageFormat.format(msg, args)); }
@Override public void error(String msg, Throwable t) { legacy.write(3, msg + " — " + t); }
}
Short answer:
class AcmePayClassAdapter extends AcmePayClient implements PaymentGateway { // class adapter
public ChargeResult charge(Money amount, String token) { /* call inherited createCharge(...) */ return null; }
}
Common trap: "a class adapter extends both the target and the adaptee". That's impossible in Java when both are classes.
Short answer:
Short answer: Bridge splits a design into two independent hierarchies:
Notification → UrgentNotification, DigestNotification;MessageSender → EmailSender, SmsSender, PushSender.The abstraction holds a reference to an implementor, the bridge. The two can then be combined freely, and extended independently: N abstractions × M implementations with N + M classes, not N × M subclasses.
Learn it in depth → Bridge & Flyweight
Short answer: Intent and timing. Bridge is designed up front, so that two dimensions can vary independently: it prevents a class explosion. Adapter is applied after the fact, to make existing, incompatible interfaces work together. Structurally, both delegate to a wrapped object. Bridge's two sides are designed together, while Adapter bolts together interfaces that weren't designed for each other.
Short answer:
public interface MessageSender { void send(String to, String body); } // the implementor
final class EmailSender implements MessageSender { public void send(String to, String b) { /* SMTP */ } }
final class SmsSender implements MessageSender { public void send(String to, String b) { /* SMS API */ } }
public abstract class Notification { // the abstraction
protected final MessageSender sender; // ← the bridge
protected Notification(MessageSender sender) { this.sender = sender; }
public abstract void notify(User user, String event);
}
final class UrgentNotification extends Notification { // refined abstraction
UrgentNotification(MessageSender s) { super(s); }
public void notify(User u, String event) { sender.send(u.contact(), "URGENT: " + event); }
}
final class DigestNotification extends Notification {
private final List<String> buffer = new ArrayList<>();
DigestNotification(MessageSender s) { super(s); }
public void notify(User u, String event) { buffer.add(event); if (buffer.size() == 10) flush(u); }
private void flush(User u) { sender.send(u.contact(), String.join("\n", buffer)); buffer.clear(); }
}
// any abstraction × any sender: new UrgentNotification(new SmsSender()), new DigestNotification(new EmailSender())
Short answer: Whenever two dimensions of variation would otherwise multiply subclasses:
Also: when you want to switch implementations at runtime, or keep platform-specific code out of the business hierarchy.
Key points to cover:
java.sql abstractions, and each vendor's driver provides the implementation, which can vary independently.Short answer:
The costs: more indirection, and an up-front design effort. Use it where both dimensions really vary.
Q: What is a two-way adapter? A: An adapter that implements both interfaces, so each side can call the other through it. It's useful during migrations, but it's more complex. Use it sparingly.
Q: How does an anti-corruption layer differ from a single adapter? A: An ACL is a whole translation boundary (often several adapters, translators and facades) that protects a bounded context's model from an external system's model. The Adapter pattern is one of its building blocks.
Q: Is Spring's HandlerAdapter an Adapter?
A: Yes. The DispatcherServlet calls every kind of handler (annotated controllers, HttpRequestHandlers, functional endpoints) through one HandlerAdapter interface, with a different adapter for each handler type.
Q: Bridge vs Strategy? A: Both delegate to an interchangeable object. Strategy varies one algorithm used by a context. Bridge separates a whole abstraction hierarchy from an implementation hierarchy, both of which are extended.