Facade vs Proxy vs Adapter Design Patterns


Introduction
These design patterns—Facade, Proxy, and Adapter—can be pretty confusing because they seem similar at first. They all sort of sit in front of something else and manage how you interact with it.
I’ve been there too, scratching my head early on trying to figure them out. But each one actually has its own purpose and use case once you understand the differences.
The Structural Design Patterns
In fact, these patterns are called Structural Design Patterns in software design. These structural patterns are basically blueprints for how to put classes and objects together to make bigger structures. They help make sure your code is flexible and efficient. You've probably used them without even realizing it!
Overview of Facade, Proxy, and Adapter
Today, we're looking at three specific structural patterns: Facade, Proxy, and Adapter. They sometimes get mixed up because they all involve wrapping an object or objects. However, their reasons for doing that wrapping are totally different.
Facade: Simplifies.
Proxy: Controls.
Adapter: Translates.
Let's break them down one by one.
Facade Pattern Overview
Have you ever felt overwhelmed by a system with way too many parts? That's where Facade comes in handy.
Purpose and Real-World Analogy
The main purpose of the Facade pattern is to provide a single, simplified interface to a larger, more complex body of code, like a library or a subsystem.
Imagine you want to start your home theater. You could turn on the TV, then the receiver, then the Blu-ray player, select the right inputs... or you could just press one button on a universal remote that does all that for you. That remote is the Facade! It hides all the messy steps behind one simple action.
Practical Use Cases in Software
In software, you might use a Facade when:
You have a complex subsystem with lots of moving parts, and you want to provide a simple entry point for clients using it.
You want to reduce dependencies between client code and the internal workings of a subsystem. If the subsystem changes internally, clients using the Facade might not even notice, as long as the Facade's interface stays the same.
You need to layer your system, and Facades can provide cleaner gateways between layers. A common example is using a Facade as an Anti-Corruption Layer in Domain-Driven Design. When your app talks to an external system, legacy code, or an untrusted system, the Facade acts as a protective wrapper. It hides the messy or incompatible interface of the external system and exposes a clean, consistent API that fits your domain model, so the rest of your code stays clean and unaffected by external quirks.
Code Example and Benefits
Let's say we have a complex system for ordering stuff online. It involves checking inventory, processing payments, and arranging shipping.
// Complex Subsystem Parts
class InventorySystem {
check(productId: string): void {
console.log(`Checking inventory for ${productId}`);
// Complex logic here...
}
}
class PaymentSystem {
processPayment(amount: number): void {
console.log(`Processing payment of ${amount}`);
// Complex logic here...
}
}
class ShippingSystem {
arrangeShipping(productId: string, address: string): void {
console.log(`Arranging shipping for ${productId} to ${address}`);
// Complex logic here...
}
}
// The Facade
class OrderFacade {
private inventory = new InventorySystem();
private payment = new PaymentSystem();
private shipping = new ShippingSystem();
placeOrder(productId: string, amount: number, address: string): void {
console.log("--- Placing Order ---");
this.inventory.check(productId);
this.payment.processPayment(amount);
this.shipping.arrangeShipping(productId, address);
console.log("Order placed successfully!");
}
}
// Client Code (much simpler now!)
const orderSystem = new OrderFacade();
orderSystem.placeOrder("product123", 49.99, "123 Main St");
/* Output:
--- Placing Order ---
Checking inventory for product123
Processing payment of $49.99
Arranging shipping for product123 to 123 Main St
Order placed successfully!
*/
See how the client just calls placeOrder
on the OrderFacade
? It doesn't need to know about InventorySystem
, PaymentSystem
, or ShippingSystem
at all.
Benefits:
Simplicity: Hides complex internal workings.
Decoupling: Reduces dependencies between clients and the subsystem. Makes the system easier to change.
When and When Not to Use
Use Facade When:
You need a simple interface to a complex system.
You want to decouple subsystems from clients and other subsystems.
You are structuring a system into layers.
Don't Use Facade When:
The system isn't actually that complex. Adding a Facade might just be unnecessary abstraction (sometimes called abstraction bloat).
Clients need access to the low-level details and fine-grained control over the subsystem's parts. The Facade might hide too much.
Proxy Pattern Overview
The Proxy pattern is all about control. It acts as a stand-in or placeholder for another object.
Purpose and Types (Virtual, Remote, Protection)
Why use a stand-in? Well, the Proxy controls access to the original object (often called the "real subject"), letting you do stuff before or after the request gets to the original object. It can be useful for lots of reasons. There are several common types:
Virtual Proxy: Delays the creation and initialization of an expensive object until it's actually needed. Think lazy loading. This is great for performance if the object isn't always used.
Remote Proxy: Represents an object that lives in a different address space (like on a server). The proxy handles the communication details, making it seem like the remote object is right there. Ever used RPC? That's often a Remote Proxy.
Protection Proxy: Controls access based on permissions. Checks if the caller has the rights to perform an operation before forwarding the request to the real object. Useful for security.
Smart Proxy (or Smart Reference): Performs additional actions when the object is accessed, like reference counting or locking.
Practical Use Cases and Code Example
Proxies pop up everywhere:
Lazy loading images or data.
API gateways acting as proxies for microservices.
Access control checks in frameworks.
Caching results from expensive operations.
Remember the Anti-Corruption Layer? If you add an authentication functionality to it, you can consider it a Facade and a Protection Proxy at the same time, though it does a lot.
Here's a simple Protection Proxy example. Imagine we only want admins to access a sensitive resource.
interface Resource {
access(): void;
}
// The Real Object (sensitive)
class SensitiveResource implements Resource {
access(): void {
console.log("Accessing the sensitive resource... secrets revealed!");
}
}
// User object (simplified)
class User {
constructor(public isAdmin: boolean) {}
}
// The Protection Proxy
class ResourceProxy implements Resource {
private realResource: SensitiveResource | null = null;
private user: User;
constructor(user: User) {
this.user = user;
}
access(): void {
if (this.user.isAdmin) {
// Lazy initialization of the real resource could also be done here (Virtual Proxy aspect)
if (!this.realResource) {
this.realResource = new SensitiveResource();
}
console.log("Proxy: Admin access granted.");
this.realResource.access();
} else {
console.log("Proxy: Access denied. User is not an admin.");
}
}
}
// Client Code
const regularUser = new User(false);
const adminUser = new User(true);
const proxyForRegular = new ResourceProxy(regularUser);
proxyForRegular.access(); // Access denied
console.log("---");
const proxyForAdmin = new ResourceProxy(adminUser);
proxyForAdmin.access(); // Access granted
/* Output:
Proxy: Access denied. User is not an admin.
---
Proxy: Admin access granted.
Accessing the sensitive resource... secrets revealed!
*/
The client interacts with the ResourceProxy
, which checks the user's permissions before deciding whether to forward the call to the SensitiveResource
.
Pros, Cons, and Performance Considerations
Pros:
Control: You can manage how and if the real object is accessed.
Lifecycle Management: Useful for lazy loading (Virtual) or managing remote connections (Remote).
Security: Can add access control layers (Protection).
Transparency: Often, the client doesn't know it's talking to a proxy.
Cons:
Complexity: Adds another layer of indirection.
Performance: The proxy itself adds some overhead. Poorly implemented proxies (especially Remote ones) can significantly impact response times.
Adapter Pattern Overview
Got two things that should work together, but their interfaces just don't match? Adapter to the rescue!
Purpose and Real-World Analogy
The Adapter pattern acts as a bridge between two incompatible interfaces. Its purpose is to convert the interface of a class into another interface that clients expect.
Think about traveling overseas; your laptop charger plug won't fit the wall socket directly. You need a travel adapter that sits in between, taking the shape of the wall socket on one side and providing the shape your plug needs on the other. That's exactly what the Adapter pattern does for code.
Class vs Object Adapters
There are two main ways to implement an Adapter:
Class Adapter: Uses multiple inheritance (or interfaces and single class inheritance) to adapt one interface to another. The Adapter inherits from both the target interface (what the client expects) and the adaptee class (the thing being adapted). This isn't always possible in languages like Java or C# that don't support multiple class inheritance, though interfaces help.
Object Adapter: Uses Composition. The Adapter holds an instance of the adaptee class. It implements the target interface and delegates calls to the adaptee object. This is generally more flexible and preferred because composition is often better than inheritance.
Use Cases and Code Example
Adapters are super useful when:
You want to use an existing class, but its interface doesn't match the one you need.
You're working with third-party libraries that you can't change.
You need to create a reusable class that cooperates with unrelated or unforeseen classes.
Let's show an Object Adapter. Imagine we have an old logging system (OldLogger
) and our application now expects a new Logger
interface.
// The interface our application expects
interface Logger {
logInfo(message: string): void;
logError(message: string): void;
}
// The old, incompatible logging class
class OldLogger {
info(text: string): void {
console.log(`[OLD INFO] ${text}`);
}
error(text: string): void {
console.log(`[OLD ERROR] ${text}`);
}
}
// The Adapter
class LoggerAdapter implements Logger {
private oldLogger: OldLogger;
constructor(oldLogger: OldLogger) {
this.oldLogger = oldLogger;
}
logInfo(message: string): void {
// Adapting the call
this.oldLogger.info(message);
}
logError(message: string): void {
// Adapting the call
this.oldLogger.error(message);
}
}
// Client Code (uses the new Logger interface)
function clientCode(logger: Logger) {
logger.logInfo("This is an informational message.");
logger.logError("This is an error message.");
}
// Create the adaptee and the adapter
const oldSystemLogger = new OldLogger();
const adapter = new LoggerAdapter(oldSystemLogger);
// Pass the adapter to the client code
clientCode(adapter);
/* Output:
[OLD INFO] This is an informational message.
[OLD ERROR] This is an error message.
*/
The LoggerAdapter
takes an OldLogger
instance and makes it usable wherever a Logger
is expected by translating the method calls (logInfo
-> info
, logError
-> error
).
Strengths and Limitations
Strengths:
Reusability: Allows you to use existing classes with new interfaces.
Flexibility: Object adapters let you adapt subclasses of the adaptee too.
Cleanliness: Keeps the client code clean; it doesn't need to know about the adaptation.
Limitations:
Complexity: Adds another class just for translation.
Efficiency: Can introduce a slight overhead due to the extra layer of indirection, though usually negligible.
Class adapters can be less flexible as they tie the adapter to a specific adaptee class.
Facade vs Proxy vs Adapter: Side-by-Side Comparison
Alright, let’s put them head-to-head. They all wrap something, but why they wrap it is the key difference.
Core Intent and Interface Differences
Facade: Simplifies. Creates a new, simpler interface for a complex system. It doesn't necessarily match any existing interface. Its goal is ease of use.
Proxy: Controls. Implements the same interface as the object it's proxying (the real subject). Its goal is to manage access, lifecycle, or add behavior without the client knowing.
Adapter: Translates. Converts one interface into another, different interface. Its goal is to make two incompatible things work together.
Use Case Scenarios
Let's map some scenarios:
Scenario: You have a complex set of APIs for video processing (encoding, adding watermarks, uploading). You want a single method
processAndUploadVideo(file)
.- Pattern: Facade. You're simplifying the complex process.
Scenario: You need to load high-resolution images in your app, but loading them all at startup is too slow. You want to load them only when they become visible.
- Pattern: Virtual Proxy. You're controlling when the expensive object (image) is loaded.
Scenario: Your application uses a
DataFetcher
interface, but you need to integrate data from a third-party service that provides aLegacyDataProvider
class with different method names.- Pattern: Adapter. You're translating the
LegacyDataProvider
interface to match theDataFetcher
interface.
- Pattern: Adapter. You're translating the
Scenario: You need to restrict certain operations on an object based on user roles (e.g., only admins can delete).
- Pattern: Protection Proxy. You're controlling access to the object's methods.
Quick Reference Comparison Table
This table sums it up nice and neat:
Aspect | Facade | Proxy | Adapter |
Purpose | Simplify a complex system | Control access to an object | Make interfaces compatible |
Interface | New, simplified interface | Same interface as the Subject | Target interface (adapts one) |
How it works | Hides subsystem complexity | Intercepts calls to Subject | Translates calls |
Problem Solved | Making subsystems easier to use | Lazy loading, access control, remote access | Using incompatible classes |
Coupling | Decouples client from subsystem | Client often unaware of proxy | Couples client to Adapter (target interface) |
Clean Implementation Tips
Keep it Focused: Ensure each pattern implementation sticks to its core intent. Don't make a Facade that also does complex access control (that's Proxy's job) or also translates interfaces (that's Adapter's job). Follow the Single Responsibility Principle.
Naming Matters: Use clear names.
OrderServiceFacade
,UserAccessProxy
,LegacyApiAdapter
immediately tell you the pattern and purpose.Prefer Object Adapters: Unless you have a very specific reason (and language support), favor composition (Object Adapter) over inheritance (Class Adapter) for flexibility.
Don't Overdo It: Sometimes, a simple function or direct call is fine. Only introduce these patterns when the complexity or incompatibility genuinely warrants it.
Common Pitfalls and Misconceptions
It's easy to get these patterns mixed up or use them incorrectly. I've seen it happen plenty of times.
Overlapping Responsibilities
The biggest mistake is creating a class that tries to be a Facade, Proxy, and Adapter all at once. This usually leads to a confusing mess that violates the Single Responsibility Principle. If you need to simplify and control access and adapt, you probably need multiple distinct classes, perhaps working together.
Misapplying Proxy as Facade or Adapter
Proxy vs Facade: Don't use a Proxy just to simplify; that's what Facade is for.
Proxy vs Adapter: A Proxy provides the same interface. An Adapter provides a different one. If you're changing the interface, you need an Adapter, not a Proxy.
Avoiding Overengineering and Abstraction Bloat
Patterns are tools, not goals. Sometimes, especially in smaller projects or simpler scenarios, adding a Facade, Proxy, or Adapter is just unnecessary complexity.
Don't add layers just because you can. Make sure the pattern actually solves a tangible problem you have right now or are very likely to have soon. Remember the Open-Closed Principle - design so you can extend without excessive modification, but don't over-design for futures that never arrive.
Conclusion
Okay, let's wrap this up quickly.
Want to make something complex easy? Use a Facade.
Want to manage or control access to an object? Use a Proxy.
Want to make two different interfaces compatible? Use an Adapter.
Think about the problem first, then see which pattern's intent best matches the solution you need. Don't force a pattern where it doesn't fit naturally.
And that’s pretty much it! Hopefully, the differences are a bit clearer now.
Think About It
If you enjoyed this article, I’d truly appreciate it if you could share it—it really motivates me to keep creating more helpful content!
If you’re interested in exploring more, check out these articles.
Thanks for sticking with me until the end—I hope you found this article valuable and enjoyable!
Subscribe to my newsletter
Read articles from Mohamed Mayallo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Mohamed Mayallo
Mohamed Mayallo
I'm a Software Engineer Passionate about Clean Code, Design Patterns, and System Design. Learning something new every day. Feel free to say Hi on LinkedIn at https://www.linkedin.com/in/mayallo