Appearance
Facade Pattern
GoF Patterns Series — Structural · See also: Adapter · Decorator
Introduction
Complex subsystems develop many classes, deep dependency graphs, and intricate interaction protocols. Forcing every client to navigate that complexity directly — knowing which classes to call, in which order, with which parameters — is a recipe for tight coupling, duplicated orchestration code, and fragile clients that break whenever the subsystem evolves. The Facade pattern addresses this by presenting a single, cohesive entry point that hides the subsystem's internal structure behind a simple, intention-revealing interface.
Intent
Provide a unified interface to a set of interfaces in a subsystem, making the subsystem easier to use by defining a higher-level interface that reduces the surface area clients must understand.
Structure
Coupling Reduction
Without the Facade, the controller knows four subsystems. With it, it knows only OrderFacade.
Interaction Flow
Participants
| Role | Responsibility |
|---|---|
Facade (OrderFacade) | Knows which subsystem classes to call, in what order, and with what inputs. Delegates all real work; adds no new functionality. |
Subsystem classes (InventoryService, PaymentService, etc.) | Perform the actual work. They have no knowledge of the Facade; they can still be called directly when needed. |
Client (OrderController) | Calls only the Facade. It is fully decoupled from subsystem implementation details. |
Anti-Pattern — Controller Orchestrating Subsystems Directly
Without a Facade, orchestration logic bleeds into controllers and is duplicated wherever order placement occurs (HTTP handler, SQS consumer, scheduled job, etc.).
java
// ANTI-PATTERN: OrderController directly orchestrating four subsystems
@RestController
@RequestMapping("/orders")
public class OrderController {
// Four subsystem dependencies — every call site that places an order needs all four
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final ShippingService shippingService;
private final NotificationService notificationService;
@PostMapping
public ResponseEntity<OrderConfirmation> placeOrder(@RequestBody OrderRequest request) {
Order order = OrderMapper.toDomain(request);
// Step 1 — caller must know inventory reservation comes first
ReservationToken reservation;
try {
reservation = inventoryService.reserveItems(order.getItems());
} catch (InsufficientStockException e) {
return ResponseEntity.badRequest()
.body(OrderConfirmation.failed("Out of stock: " + e.getMessage()));
}
// Step 2 — caller must know payment is two-phase (authorize then capture)
AuthToken authToken;
try {
authToken = paymentService.authorize(order.getCustomerId(), order.getTotal());
} catch (PaymentDeclinedException e) {
// Caller must remember to roll back inventory
inventoryService.releaseReservation(reservation);
return ResponseEntity.badRequest()
.body(OrderConfirmation.failed("Payment declined: " + e.getMessage()));
}
ChargeReceipt receipt = paymentService.capture(authToken);
inventoryService.commitReservation(reservation);
// Step 3 — caller must know shipment creation parameters
TrackingNumber tracking = shippingService.createShipment(
order.getId(), order.getShippingAddress(), order.getItems());
// Step 4 — caller must assemble all context for the notification
notificationService.sendOrderConfirmation(order, receipt, tracking);
return ResponseEntity.ok(new OrderConfirmation(order.getId(), tracking, receipt));
}
}Problems with this design:
- The four-step protocol (reserve → authorize → capture → commit → ship → notify) is duplicated in every entry point that places an order — HTTP controller, SQS listener, batch job.
- Any change to the protocol (adding a fraud check, reordering capture and commit) requires finding and updating every call site.
- The rollback logic on payment decline (
releaseReservation) is easy to forget in new call sites, creating inventory leaks. OrderControllerhas four dependencies on subsystem services, making it harder to test and extend.- The controller violates the Single Responsibility Principle: it handles both HTTP concerns and business orchestration.
Correct Implementation
Step 1 — The Facade encapsulates the entire protocol.
java
@Service
public class OrderFacade {
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final ShippingService shippingService;
private final NotificationService notificationService;
private final OrderRepository orderRepository;
public OrderFacade(InventoryService inventoryService,
PaymentService paymentService,
ShippingService shippingService,
NotificationService notificationService,
OrderRepository orderRepository) {
this.inventoryService = inventoryService;
this.paymentService = paymentService;
this.shippingService = shippingService;
this.notificationService = notificationService;
this.orderRepository = orderRepository;
}
public OrderConfirmation placeOrder(Order order) {
// Step 1: Reserve inventory
ReservationToken reservation;
try {
reservation = inventoryService.reserveItems(order.getItems());
} catch (InsufficientStockException e) {
throw new OrderPlacementException("Out of stock: " + e.getMessage(), e);
}
// Step 2: Authorize and capture payment, rolling back inventory on failure
AuthToken authToken;
try {
authToken = paymentService.authorize(order.getCustomerId(), order.getTotal());
} catch (PaymentDeclinedException e) {
inventoryService.releaseReservation(reservation); // always released on decline
throw new OrderPlacementException("Payment declined: " + e.getMessage(), e);
}
ChargeReceipt receipt = paymentService.capture(authToken);
inventoryService.commitReservation(reservation);
// Step 3: Fulfill
TrackingNumber tracking = shippingService.createShipment(
order.getId(), order.getShippingAddress(), order.getItems());
order.setStatus(OrderStatus.CONFIRMED);
orderRepository.save(order);
// Step 4: Notify — fire-and-forget; never fails a placed order
notificationService.sendOrderConfirmation(order, receipt, tracking);
return new OrderConfirmation(order.getId(), tracking, receipt);
}
public CancellationResult cancelOrder(String orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
if (order.getStatus() == OrderStatus.SHIPPED) {
return CancellationResult.failed("Order already shipped");
}
// Coordinator owns the reversal sequence — no client needs to know it
shippingService.cancelShipment(order.getTrackingNumber());
paymentService.void_(order.getPaymentAuthToken());
inventoryService.releaseReservation(order.getReservationToken());
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
notificationService.sendCancellationNotice(order);
return CancellationResult.ok();
}
}Step 2 — The controller shrinks to its actual responsibility: HTTP I/O.
java
@RestController
@RequestMapping("/orders")
public class OrderController {
// One dependency — the Facade
private final OrderFacade orderFacade;
public OrderController(OrderFacade orderFacade) {
this.orderFacade = orderFacade;
}
@PostMapping
public ResponseEntity<OrderConfirmation> placeOrder(@RequestBody OrderRequest request) {
Order order = OrderMapper.toDomain(request);
try {
return ResponseEntity.ok(orderFacade.placeOrder(order));
} catch (OrderPlacementException e) {
return ResponseEntity.badRequest()
.body(OrderConfirmation.failed(e.getMessage()));
}
}
@DeleteMapping("/{orderId}")
public ResponseEntity<CancellationResult> cancelOrder(@PathVariable String orderId) {
return ResponseEntity.ok(orderFacade.cancelOrder(orderId));
}
}An SQS consumer that also places orders calls the same Facade and does not repeat any orchestration logic:
java
@SqsListener("order-requests-queue")
public void onOrderRequest(OrderRequest request) {
Order order = OrderMapper.toDomain(request);
orderFacade.placeOrder(order); // same four-step protocol, zero duplication
}Real-World Examples
| Example | Subsystem Complexity Hidden |
|---|---|
AWS SDK S3TransferManager | Multipart upload management, part size calculation, parallel threads, retry, checksum verification — all behind upload(UploadFileRequest) |
Spring JdbcTemplate | Connection acquisition, PreparedStatement creation, parameter binding, ResultSet mapping, finally-block cleanup — all behind query(sql, rowMapper) |
| SLF4J | Selects Logback, Log4j 2, or JUL at runtime; caller only sees LoggerFactory.getLogger() and logger.info() |
| Spring Data repositories | JPA session management, JPQL generation, dirty-checking, flush strategy — all behind UserRepository.save() |
java.net.URL.openConnection() | Protocol selection (HTTP, FTP, file), connection negotiation, stream wrapping |
S3TransferManager is an especially clear AWS example. Uploading a 5 GB object to S3 requires splitting it into parts, uploading each in parallel, tracking ETags, retrying failed parts, and calling CompleteMultipartUpload. S3TransferManager.upload() hides all of that behind one call. The raw S3Client still exists and remains accessible when fine-grained control is needed — the Facade does not remove subsystem access, it just makes the common case simple.
java
// Without S3TransferManager — the client orchestrates multipart protocol directly
S3Client s3 = S3Client.create();
CreateMultipartUploadResponse create = s3.createMultipartUpload(b -> b.bucket(bucket).key(key));
String uploadId = create.uploadId();
List<CompletedPart> parts = new ArrayList<>();
// ... split file, upload each part, collect ETags, handle retries ...
s3.completeMultipartUpload(b -> b.bucket(bucket).key(key)
.uploadId(uploadId)
.multipartUpload(m -> m.parts(parts)));
// With S3TransferManager — the facade handles all of the above
S3TransferManager tm = S3TransferManager.create();
FileUpload upload = tm.uploadFile(r -> r.putObjectRequest(p -> p.bucket(bucket).key(key))
.source(filePath));
upload.completionFuture().join();Facade vs. Adapter vs. Mediator
These three patterns are frequently confused because they all involve one object interacting with others.
| Facade | Adapter | Mediator | |
|---|---|---|---|
| Goal | Simplify access to a complex subsystem | Translate between incompatible interfaces | Centralise communication between many objects |
| Direction | One-directional: client → subsystem | One-directional: client → adaptee | Bi-directional: objects communicate through the mediator |
| Subsystem knowledge | Subsystem classes are unaware of the Facade | Adaptee is unaware of the Adapter | Colleagues know the Mediator |
| Interface relationship | Client uses a new, simpler interface | Adapter makes adaptee match an existing interface | Colleagues depend on a shared Mediator interface |
| Typical use | Orchestrating multiple subsystems for common workflows | Wrapping third-party or legacy APIs | UI widget coordination, event buses, workflow engines |
The Facade simplifies. The Adapter translates. The Mediator coordinates bidirectionally.
See Adapter for the translation pattern in detail.
When to Use
- A complex subsystem is difficult to understand because it exposes many classes, and clients must call them in a specific order that is not obvious from the individual class APIs.
- You want to layer a subsystem: provide a simple interface for common use cases while still allowing advanced clients to access the lower-level classes directly.
- Multiple entry points (HTTP, messaging, batch) perform the same orchestration — the Facade eliminates the duplication.
- You want to reduce compile-time coupling between clients and subsystem classes so that internal refactoring does not ripple outward.
Consequences
Benefits
- Reduced coupling. Clients depend on one class (the Facade) rather than on each subsystem class. Internal refactoring stays invisible to clients.
- Eliminates duplicated orchestration. The protocol for placing an order lives in one place. Every entry point calls the Facade; none repeat the logic.
- Separation of concerns. Controllers handle HTTP; the Facade handles business orchestration; subsystem classes handle their specific domain tasks.
- Simplified testing. Controllers are tested by mocking a single
OrderFacade. Subsystem classes are tested independently. The Facade itself is integration-tested against real subsystems. - Layered access. The Facade does not prevent advanced clients from bypassing it and calling subsystem classes directly when fine-grained control is required.
Trade-offs
- God object risk. A Facade that grows to own all orchestration for an entire domain becomes a large, hard-to-test class. Keep Facades narrow: one per workflow or bounded context, not one per service.
- Hidden complexity, not eliminated complexity. The subsystem's complexity is still there. The Facade makes it manageable for clients but does not reduce it internally.
- Premature simplification. If a subsystem is already simple, adding a Facade adds an indirection layer with no benefit. Apply the pattern when client code visibly suffers from subsystem complexity.
Related Concepts
- Adapter — translates between incompatible interfaces; the Facade simplifies access to a compatible but complex set of interfaces.
- Decorator — adds behavior to an object implementing the same interface; the Facade wraps multiple different classes behind a new interface.
- Single Responsibility Principle — the Facade gives orchestration logic a dedicated home, separating it from HTTP, messaging, and other delivery concerns.
- Clean Architecture — the Facade is a natural fit for the Use Case layer, which orchestrates entities and ports without exposing subsystem structure to the delivery layer.
- Hexagonal Architecture — a Driving Port (inbound interface) often corresponds to a Facade: one entry point per use case that hides internal port and adapter wiring from the caller.
- Saga Pattern — when the Facade orchestrates distributed transactions across microservices, the coordination logic it hides becomes a Saga.