Question I get often from junior engineers: "of all the GoF patterns, which ones do you actually reach for in real work". My honest answer is two structural patterns and a couple of creational ones. Strategy and Factory Method I use weekly. Observer is mostly handled by frameworks, so I rarely write it from scratch. Singleton I avoid. Builder I use for one specific shape. Decorator I treat as a language feature, not a class hierarchy.
The two structural patterns I genuinely reach for, and that pay off every time, are Adapter and Facade. They look similar in the textbook ("both wrap something") but they solve different problems and the small details of each one are what makes them worth knowing.
Adapter: when you can't change the other side
Adapter is the pattern you use when you have an interface your code wants to consume and an existing implementation that almost-but-not-quite fits it. You cannot or should not change the existing implementation (it is a third-party library, a legacy module, a SaaS SDK), so you wrap it in a small class or function that exposes the shape your code wants.
A real example I shipped last year:
Three things to notice about that snippet, because they are the points where engineers get the pattern wrong.
First, the adapter is small. It does one thing: translate a call shape. It does not add caching, it does not validate inputs, it does not log. Each of those would be a separate layer; smushing them into the adapter is how adapters become unmaintainable wrappers a year later. If the adapter file is more than thirty lines, I look hard for what should not be in there.
Second, the adapter conforms to MY interface, not the library's. The whole point is that my code depends on CountryLookup, and the existence of LegacyCountryAdapter is invisible to the consumer. If I swap legacy-country-data for a different library next quarter, I write a new adapter and I do not touch a single consumer.
Third, the adapter handles the impedance mismatch (undefined vs null, the wider returned object vs the narrower required one). That translation is exactly the work the pattern is supposed to do. It is also where bugs hide: if getCountryNameAndCapital returns null instead of undefined for some inputs, the result ? ... : null line silently does the wrong thing. I write a focused unit test for the adapter alone with the values that actually return from the library, never the values the docs say are returned.
When I would NOT use Adapter: when I control both sides. If the implementation lives in my codebase too, I just change its signature. The adapter only earns its keep when the other side is something I cannot or should not modify.
Facade: when YOUR side is the mess
Facade is the pattern for the opposite problem. The implementation is yours, but it has grown to ten classes that all need to be coordinated to do anything useful. Calling code has to know about all of them, instantiate them in the right order, and pass results between them. The Facade is a single class (or function) that hides the orchestration behind one well-named entry point.
A shape I have shipped more than once:
The call site that used to read await emailVerifier.verifyAvailable(email); const hash = await hasher.hash(...) ... six lines deep is now await userSignup.signUp({...}). The orchestration moved into one place, and the call site is decoupled from how many internal services participate.
Four things that make a Facade pull its weight, and that I check during code review:
- The Facade does not LEAK its internals. The signature accepts a small input record and returns a result that does not expose
EmailVerifierorPasswordHasher. If my callers see the inner objects, the Facade is a bag of pass-throughs and not actually hiding anything. - The Facade has a clear responsibility I can name in one sentence ("sign a user up"). If I find myself writing
class Manager extends Managerorclass UserService { signUp(); login(); resetPassword(); deleteAccount(); ... }, the Facade has grown into a god object. Split it. - The Facade does not become a replacement for the underlying objects. Other code can still depend on
UserRepodirectly. The Facade is one ergonomic API, not the only API. - The Facade is testable WITHOUT mocking everything. If exercising
UserSignup.signUprequires six mocks for tests to pass, the Facade is too coupled and its dependencies should themselves be smaller. I write integration-style tests for facades and unit tests for the inner pieces.
When I would NOT use Facade: when there is only one thing being wrapped. A class that delegates every method to one other class is not a Facade, it is a layer of indirection. The pattern requires that the Facade is coordinating multiple collaborators.
How they differ in one line
Adapter is for interface mismatch when you cannot change the other side. Facade is for orchestration when YOUR side has grown too coordinated to call directly. Same general shape (a wrapper class with simpler methods), opposite problem.
A failure mode that catches both
The failure mode shared by Adapter and Facade is the same: the wrapper grows beyond translation or orchestration and starts holding business logic. An adapter that decides which countries are eligible for shipping is no longer an adapter; it is a domain rule with an adapter's name. A Facade that calculates the user's tier as a side effect of signup is no longer a Facade; it is a service with too much going on.
The rule I keep on my whiteboard: a wrapper has the right amount of code if removing it leaves either an interface mismatch or an orchestration problem. If removing it leaves missing business logic, the wrapper has been doing things it should not.
Two patterns, two doors I open often
Both of these patterns survived the last forty years for the same reason: they solve a class of problem that does not go away. Codebases keep depending on libraries with awkward shapes, and codebases keep accumulating clusters of classes that need a clean entry point. Adapter handles the first; Facade handles the second. I have stopped reaching for most of the GoF book, but those two are the ones I keep on the workbench, and the small disciplines I described (keep them small, keep them honest, do not let business logic creep in) are most of the difference between a wrapper that helps and a wrapper that becomes the next round of tech debt.
