Adapter and facade
Part of the free Low-Level Design course on LogicWiz, in the chapter: Structural patterns. The class-diagram exercises run in the app.
Week 6: Two payment providers, two shapes
"Razorpay's SDK wants paise as a long. PayU's wants rupees as a String. Our checkout now has both, with an if around every call."
Arjun, showing Priya the checkout diff
Two providers that do not fit
SwiftKart is adding PayU as a backup for when Razorpay is down. In Week 4, Arjun gave checkout its own PaymentGateway interface. But each provider ships an SDK (software development kit: the library a vendor gives you to call its service). Each SDK has its own shape, and you cannot edit vendor code.
Here is the checkout code Arjun wrote first. The SDK calls are simplified, not the real APIs.
public Receipt pay(Cart cart, String provider) {
if (provider.equals("RAZORPAY")) {
RzpPayment p = razorpay.createPayment(cart.totalPaise(), "INR");
if (!p.status().equals("captured")) {
throw new PaymentFailedException(p.errorCode());
}
return new Receipt(p.id(), cart.totalPaise());
} else {
String rupees = String.valueOf(cart.totalPaise() / 100.0);
Map<String, String> r = payU.charge(cart.id(), rupees);
if (!"success".equals(r.get("status"))) {
throw new PaymentFailedException(r.get("error"));
}
return new Receipt(r.get("txnid"), cart.totalPaise());
}
}
Look at what leaked into checkout:
- Units: paise as a number for one, rupees as text for the other.
- Status values: "captured" for one, "success" for the other.
- Error formats: an error code field, or a map entry.
Refunds, status checks and webhooks will each need the same if. There is also a bug. String.valueOf(24950 / 100.0) gives "249.5". A provider that expects "249.50" would reject it.
Adapter: make their class fit your interface
An adapter is a class that implements the interface your code expects, and translates each call into calls on an existing class with a different interface. A travel plug adapter works the same way: the socket does not change, and the plug does not change. The adapter sits between them.
public interface PaymentGateway {
Receipt charge(String orderId, long amountPaise);
}
public class PayUAdapter implements PaymentGateway {
private final PayUApi payU;
public PayUAdapter(PayUApi payU) {
this.payU = payU;
}
@Override
public Receipt charge(String orderId, long amountPaise) {
String rupees = BigDecimal.valueOf(amountPaise, 2).toPlainString();
Map<String, String> result = payU.charge(orderId, rupees);
if (!"success".equals(result.get("status"))) {
throw new PaymentFailedException(result.get("error"));
}
return new Receipt(result.get("txnid"), amountPaise);
}
}
BigDecimal.valueOf(24950, 2) is exactly 249.50, so the rounding bug is gone, and it is fixed in one place. A RazorpayAdapter does the same for Razorpay. Checkout sees only PaymentGateway.
classDiagram
class PaymentGateway {
<<interface>>
+charge(orderId, amountPaise) Receipt
}
CheckoutService --> PaymentGateway
PaymentGateway <|.. RazorpayAdapter
PaymentGateway <|.. PayUAdapter
RazorpayAdapter --> RazorpayClient
PayUAdapter --> PayUApi
An adapter translates. It converts units, names, status values and exceptions. It should not hold business rules, such as "retry three times" or "block orders over 10,000 rupees". Those belong in checkout or in their own class.
Adapters translate errors too. A vendor timeout or a vendor exception becomes your own PaymentFailedException, so checkout catches one type for every provider. Testing also gets easier. Checkout's tests use a fake that implements PaymentGateway. Only the adapter's own tests need the vendor SDK, usually against the provider's test mode.
This version holds the vendor object in a field. That is an object adapter. A class adapter would extend the vendor class instead. In Java a class can extend only one class, and extending the vendor class exposes all its methods to your code. Prefer holding it.
⚠️ Warning: Never return a vendor type, such as
RzpPayment, from an adapter method. Checkout would depend on Razorpay again, and the adapter would protect nothing.
When is an adapter the wrong tool? When you own both sides. If two of your own classes do not fit, change one of them. An adapter is for code you cannot or should not edit: vendor SDKs, old modules owned by another team, generated code.
Facade: one simple door to many classes
Now look at what happens when a customer taps "Place order". The HTTP controller makes these calls, in this order:
Cart cart = carts.cartFor(userId);
inventory.reserve(cart.lines());
long total = pricing.total(cart, couponCode);
Receipt receipt = gateway.charge(cart.id(), total);
Order order = orders.save(Order.paid(cart, receipt));
inventory.commit(cart.lines());
notifier.orderPlaced(order);
The mobile API, the web API and the "reorder" button each have a copy of these seven lines. One copy forgot inventory.commit. None of them releases the stock when payment fails.
A facade is a class that gives callers one simple method for a common task that needs several classes. The classes behind it are called the subsystem.
public class CheckoutFacade {
// carts, inventory, pricing, gateway, orders and notifier, passed in through the constructor
public Order placeOrder(String userId, String couponCode) {
Cart cart = carts.cartFor(userId);
inventory.reserve(cart.lines());
try {
long total = pricing.total(cart, couponCode);
Receipt receipt = gateway.charge(cart.id(), total);
Order order = orders.save(Order.paid(cart, receipt));
inventory.commit(cart.lines());
notifier.orderPlaced(order);
return order;
} catch (PaymentFailedException e) {
inventory.release(cart.lines());
throw e;
}
}
}
All three callers now make one call, checkout.placeOrder(userId, couponCode). The order of steps, and the clean-up on failure, live in one place. A facade does not lock the subsystem away. An admin tool can still call InventoryService directly.
Arjun asks, "Is a facade a god class with a nicer name?" It does not have to be. A facade coordinates; it does not compute. The coupon rules stay in PricingService. If the facade starts holding rules, or grows thirty methods, split it by use case: CheckoutFacade, ReturnsFacade, ReorderFacade.
Should the facade itself be an interface? Usually not at first. Its job is to call other interfaces, and callers can use the class directly. Add an interface later if a controller test needs to fake the whole checkout.
Adapter, facade or neither
Both patterns wrap other code, so people mix them up. The difference is how many classes they wrap, and why.
| Adapter | Facade | |
|---|---|---|
| Wraps | one existing class | several classes |
| Interface it offers | one your code already expects | a new, simpler one |
| Problem it solves | the shapes do not match | callers repeat a long sequence |
Use neither when it adds a layer with no job. A facade over one class with one method is only an extra hop. An adapter between two classes you own hides a mismatch you could fix directly.
In an interview, say the problem first. "The two SDKs have different shapes, so I will put each behind an adapter that implements our PaymentGateway." That sentence shows the interviewer you know why, not only what.
📌 Summary: An adapter makes one existing class fit an interface you own, translating units, statuses and errors. A facade gives callers one simple method over several classes. Neither should hold business rules.
What Arjun learns next
Adapters change an interface. Next, Arjun meets three patterns that keep the interface the same: decorator for price add-ons, composite for category trees, and proxy for caching.
Check your understanding
- SwiftKart adds Paytm as a third payment provider, with its own SDK. With the adapter design, what changes?
- RazorpayAdapter.charge returns the SDK's RzpPayment object instead of SwiftKart's Receipt. What is wrong?
- CheckoutFacade starts computing coupon discounts inside placeOrder. What should Priya say in review?
- Arjun's own OrderExporter produces rows, and his own ReportWriter expects a slightly different row format. Both belong to his team. What is the best fix?