Welcome to the Structural patterns, where the question shifts from how objects are created to how they fit together. First up is the one you’ve almost certainly used already, maybe without naming it. Adapter lets your code speak its own language while a small class translates for a third party that speaks a different one.
| Category | Structural |
| Smell fixed | S06: Checkout depends directly on the courier’s XML API |
| Code | patterns/structural/adapter |
The Scene
CornerStore ships through a courier called ShipFast. Every successful checkout books a consignment:
[060] checkout customer=cu-1 payment=card
card charged A$300.33 for Ava
ShipFast booked SF-0001 for o-1001 (30033 cents)
Notice the 30033 cents. CornerStore thinks in decimal dollars. ShipFast thinks in whole cents, and it only speaks XML. Someone has to translate. The question is where that translation lives.
The Smell
In the baseline, it lives in checkout, inside StoreConsole:
// SMELL S06: checkout calls the courier client with XML and parses its response directly
private string BookShipment(Order order)
{
var cents = Math.Round(order.Quote.Total * 100m, 0, MidpointRounding.AwayFromZero)
.ToString("0", CultureInfo.InvariantCulture);
var name = System.Security.SecurityElement.Escape(order.Customer.Name) ?? "";
var address = System.Security.SecurityElement.Escape(order.Customer.Address) ?? "";
var request = "<shipment><orderId>" + order.Id + "</orderId><recipient>" + name
+ "</recipient><address>" + address + "</address><amountCents>" + cents + "</amountCents></shipment>";
var response = shipFastClient.CreateConsignmentFromXml(request);
return XDocument.Parse(response).Root?.Element("tracking")?.Value ?? "";
}
Rounding to cents, XML escaping, element names, response parsing. None of that is about selling things. It’s all about how one vendor’s API happens to work.
That matters the day ShipFast changes its API, or the business adds a second courier. Your checkout code changes because a vendor did. Vendor details have leaked into the heart of your system.
The Idea
An Adapter sits between your code and someone else’s. Your code calls an interface that you designed, in your own terms. The adapter implements that interface by translating to and from the other side.
It’s the travel adapter in your suitcase. Your laptop charger doesn’t change when you land in another country. The wall socket doesn’t change either. A small plug in the middle makes them fit.
The Refactor
First, CornerStore defines what it actually wants from a courier, in its own language:
internal interface IShippingProvider
{
string BookShipment(Order order);
}
Give it an order, get back a tracking number. No XML, no cents.
Then the adapter implements that interface by wrapping the existing ShipFast client:
internal sealed class ShipFastAdapter(ShipFastClient client) : IShippingProvider
{
public string BookShipment(Order order)
{
var cents = Math.Round(order.Quote.Total * 100m, 0, MidpointRounding.AwayFromZero)
.ToString("0", CultureInfo.InvariantCulture);
And checkout shrinks to one line in domain language:
order.TrackingNumber = shippingProvider.BookShipment(order);
If the adapter’s body looks familiar, it should. It’s almost exactly the code that used to live in StoreConsole. That’s typical of Adapter. You’re rarely writing new logic. You’re moving translation logic to the boundary, where it belongs, and giving the rest of the system a clean contract to depend on.
Also note what didn’t change: ShipFastClient itself. In real life, that’s often a NuGet package or vendor SDK you can’t modify anyway. Adapter works around it rather than through it.
classDiagram
StoreConsole --> IShippingProvider : books shipment
IShippingProvider <|.. ShipFastAdapter
ShipFastAdapter --> ShipFastClient : translates request and response
The book calls IShippingProvider the target, ShipFastAdapter the adapter and ShipFastClient the adaptee.
What It Cost
Small and focused: 5 files, about 94 lines added and 17 removed. Most of the additions are tests and the new interface.
The payoff is out of proportion to the cost. Adding a second courier is now a second adapter, with no change to checkout at all.
When Not to Use It
- The interface already fits. If a library’s API is already a good match for your domain, wrapping it adds a layer that translates nothing.
- You’re wrapping for the sake of wrapping. “Wrap every third-party call just in case” leads to thin adapters that mirror the vendor API one-to-one. That gives you all the indirection and none of the protection. An adapter earns its place when there’s a real mismatch to absorb.
Easily Confused With
- Bridge. Both put an interface between two sides, but for different reasons. Adapter makes two existing things fit after the fact. Bridge is designed up front so two dimensions can vary independently. See Adapter vs Bridge.
- Decorator. A decorator keeps the same interface and adds behaviour. An adapter changes the interface.
- Facade. A facade simplifies a whole subsystem behind a new entry point. An adapter converts one interface into another.
Try It Yourself
dotnet build patterns/structural/adapter/Adapter.slnx
dotnet test patterns/structural/adapter/Adapter.slnx
dotnet run --project patterns/structural/adapter/src/CornerStore/CornerStore.csproj -- scenarios/trading-day.json
Exercise: write a fake IShippingProvider that returns "TEST-001" and use it in a test. Notice how easy it is now to test checkout without any XML at all.
The Adapter README has the participant table and a sequence diagram. The full change is in DIFF.md.