The patterns so far fine-tune relationships between a few pieces: two interfaces that don't fit, two hierarchies, a tree, an onion of layers. Facade operates at a different scale: when an entire subsystem —half a dozen services with their ordering, their rules, and their errors— must be used by many clients, somebody has to know how to orchestrate it. The design question is: does that knowledge live duplicated in every client, or does it live once behind a simple door? In PideYa the answer is being begged for by the checkout: confirming an order today requires every client application to coordinate five subsystems by hand.
Contents
- The problem in PideYa: the scattered checkout
- Intent and structure of the pattern
- Java implementation:
CheckoutFacade - What a facade is NOT
- Facades per layer and in public APIs
- When to use it and when not to
- Common mistakes, exercises, and conclusion
The problem in PideYa: the scattered checkout
Confirming an order in PideYa involves, in order, with error handling at every step:
- Validate the cart: dishes available (the Composite menu knows how to answer), the restaurant's minimum order amount, address inside the delivery zone.
- Compute taxes: with the
TaxCalculatorof the right market — the per-country family we locked down with Abstract Factory. - Charge: with the market's
PaymentGateway(Redsys, Conekta, or thePayPalAdapter); if the payment fails, no trace of the order must remain. - Create and persist the order: assembling it with the
Order.Builderand saving it in the repository. - Notify: the customer (confirmation) and the restaurant (new kitchen ticket), via
NotificationService.
That knowledge —what to call, in which order, what to do if step 3 fails after step 2— is today copied into every client: the iOS app, the Android app, the web, the phone-support desk. The consequences are the expected ones:
- Duplication with divergence: the web validates the minimum amount before taxes; Android, after. A fix to the flow must be repeated four times over, and some copy always falls behind.
- Massive coupling: four clients each know five subsystems (20 dependencies); changing the signature of one internal service shakes every application.
- Knowledge in the wrong layer: the rule "if the charge fails, release the cart and don't notify" is pure business, and it lives in user-interface code.
The first instinct might be "let's simplify the services". But the services are fine: each does one thing (SRP) and carries its legitimate complexity. What's excessive is not the complexity of the pieces: it is coordination knowledge scattered across the clients.
Intent and structure of the pattern
Intent (GoF): provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
The solution is almost anticlimactic in its simplicity: one class that offers the high-level operations clients actually need ("confirm this cart with this payment method") and that internally knows and coordinates the subsystem. All the pattern's sophistication lies in getting that high-level interface right, not in its mechanics.
classDiagram
class CheckoutFacade {
+confirmOrder(cart: Cart, paymentData: PaymentData) OrderConfirmation
}
class CartValidator { +validate(cart) }
class TaxCalculator { <<interface>> +compute(base) BigDecimal }
class PaymentGateway { <<interface>> +charge(amount, data) PaymentResult }
class OrderRepository { +save(order) }
class NotificationService { +notifyConfirmation(order) }
class MobileApp
class PideYaWeb
class PhoneSupport
MobileApp --> CheckoutFacade
PideYaWeb --> CheckoutFacade
PhoneSupport --> CheckoutFacade
CheckoutFacade --> CartValidator
CheckoutFacade --> TaxCalculator
CheckoutFacade --> PaymentGateway
CheckoutFacade --> OrderRepository
CheckoutFacade --> NotificationService
| GoF role | In PideYa |
|---|---|
| Facade | CheckoutFacade |
| Subsystem classes (unaware of the facade) | CartValidator, TaxCalculator, PaymentGateway, OrderRepository, NotificationService |
| Client | Mobile apps, web, phone support |
The geometry tells the story: before, a graph of 4 clients × 5 services; after, a funnel. Clients depend on one class; the subsystem doesn't know the facade exists (arrows going one way only). The dependency count drops from 20 to 4 + 5, and above all, the business flow is now written once.
Java implementation: CheckoutFacade
In module 2 an embryo of this idea appeared: that CheckoutService charging through the market family. The facade completes it with the whole flow, and receives its pieces injected —the market-dependent ones fresh out of the MarketFactory—:
public class CheckoutFacade {
private final CartValidator validator;
private final TaxCalculator taxes;
private final PaymentGateway gateway;
private final OrderRepository repository;
private final NotificationService notifications;
public CheckoutFacade(CartValidator validator,
MarketFactory marketFactory,
OrderRepository repository,
NotificationService notifications) {
this.validator = validator;
this.taxes = marketFactory.createTaxCalculator(); // coherent family
this.gateway = marketFactory.createPaymentGateway(); // from module 2
this.repository = repository;
this.notifications = notifications;
}
/** THE high-level operation: the entire confirmation flow, one single call. */
public OrderConfirmation confirmOrder(Cart cart, PaymentData paymentData) {
// 1. Validate: availability, minimum amount, delivery zone.
validator.validate(cart); // throws InvalidCartException
// 2. Taxes for the right market.
BigDecimal base = cart.getTotalBeforeTaxes();
BigDecimal tax = taxes.compute(base);
BigDecimal total = base.add(tax);
// 3. Charge. If it fails, everything ends here: there is no order to undo yet.
PaymentResult result = gateway.charge(total, paymentData);
if (!result.isAccepted()) {
throw new PaymentRejectedException(result.getReason());
}
// 4. Create the order (module 2's Builder) and persist it.
Order order = Order.builder(cart.getCustomer(), cart.getRestaurant())
.lines(cart.getLines())
.totalAmount(total)
.paymentReference(result.getTransactionId())
.build();
repository.save(order);
// 5. Notify customer and restaurant.
notifications.notifyConfirmation(order);
return new OrderConfirmation(order.getNumber(), total, order.getEstimatedTime());
}
}And the four clients are reduced to their own concern — user interface:
// In the mobile app, the web, or wherever:
OrderConfirmation conf = checkoutFacade.confirmOrder(cart, paymentData);
showSuccessScreen(conf);Details that make this a good facade:
- Ordering with intent: charging before persisting makes "undo the order" unnecessary if the payment fails. The most delicate decision in the flow is made once, in the right place, and commented.
- It speaks the client's language: it takes a
CartandPaymentData, returns a compactOrderConfirmation(number, total, estimated time) — it exposes neitherPaymentResult, nor the wholeOrder, nor internal subsystem types. A facade that leaks internal types outward is a tunnel, not a door. - It composes what was already built: per-market coherence is still guaranteed by the
MarketFactory; order assembly, by its Builder; sending, by the notification service (with its retry decorators, invisible from here). The facade reimplements nothing: it coordinates.
What a facade is NOT
The pattern is often misread through excess. Three important boundary lines:
It does not forbid direct access to the subsystem. The GoF is explicit: clients that need the full power can keep using the subsystem's classes. PideYa's admin panel queries OrderRepository directly for its listings, and that is correct: the facade offers a shortcut for the common cases, not a mandatory tollbooth. (A team may additionally decide to make it the sole entry point of a module — a legitimate architecture decision, but one on top of the pattern.)
It adds no state or business logic of its own. The facade coordinates; the rules live in the subsystem. If CheckoutFacade started computing discounts or keeping order counters, it would stop being a facade and become one more service — aggravated by the fact that its central position makes it a magnet for responsibilities: the direct road to the God Object we will catalog among the anti-patterns.
It is not "any class that calls other classes". The pattern's hallmark is the asymmetry of complexity: a simple high-level interface in front, a complex subsystem behind, and clients who thanks to it don't know the subsystem. If the "facade" has one method per internal method, it is a handrail adding indirection without subtracting complexity.
Facades per layer and in public APIs
The idea scales beyond a single class:
- Facades per layer: in layered architectures, each large subsystem exposes its facade and hides its guts:
KitchenFacade(ticket intake and statuses),DeliveryFacade(courier assignment, tracking),CheckoutFacade. Upper layers talk only to facades; teams can reorganize the inside of their subsystem without breaking anyone. It is the pattern acting as a module boundary — an embryo of what, at another scale, services will do in modern architectures (mention only). - Facades in public APIs: when PideYa publishes its partner API ("create order", "check status"), that API will be an institutional facade: high-level operations, types owned by the boundary, and no trace of the twenty internal services. Libraries do the same:
SLF4J("Simple Logging Facade for Java") is a self-declared facade over the logging subsystems;javax.xml.parsersis one over the XML-parsing machinery. - Facade + Adapter, the border duo: facing outward, a facade simplifies what's ours for others; an adapter translates what's foreign for us. At the border of every healthy system you usually find both, each looking in a different direction.
When to use it and when not to
Use it when:
- Several clients repeat the same coordination of a subsystem (the checkout symptom: a flow copied around with divergences).
- You want to stratify: define entry points per layer or module and reduce coupling between subsystems.
- A delicate business flow (ordering, transactionality, errors) deserves to live written once, with tests of its own.
- You publish an API —internal between teams or external to partners— and need a stable surface that decouples consumers from your internal evolution.
Don't use it when:
- There is a single client and a single simple call: wrapping two calls in a new class is indirection with no return (KISS).
- You would use it to hide a bad design: if the subsystem is an incomprehensible knot, the facade only hangs a curtain in front of it; the knot keeps growing behind. First tidy up (perhaps with the other patterns in this module); then, if it still adds value, facade.
- Every client needs a different flow: a facade with fifteen overloaded variants of
confirmOrder(...)is the original duplication, now centralized and with boolean parameters.
Relationship to other patterns (mentions only): Adapter translates one interface, Facade simplifies many — the fine-grained head-to-head comes in the comparison; the facade is usually created by module 2's factories and shared as a single instance (Singleton or, better, injection); Mediator also centralizes interactions, but between colleagues who talk to each other with two-way traffic — we will distinguish them in module 4; and inside the subsystem, the facade coexists frictionlessly with everything already seen.
Common Mistakes and Tips
- The facade that fattens up. Every sprint someone adds "just one more little method" and two years later
CheckoutFacadehas 40 methods and logic of its own. Watch the scale: if use cases grow, create facades per area (checkout, tracking, ratings), not one universal facade. - Leaking internal types. Returning
PaymentResultor JPA entities from the subsystem through the facade re-couples clients to what we wanted to hide. The boundary defines its own types (OrderConfirmation), just as we demanded of adapters. - A mandatory facade by dogma. Forbidding all direct access to the subsystem "because there's a facade" forces duplicating legitimate low-level operations inside it. Remember: the pattern simplifies the common case; it swears no exclusivity.
- Smuggled business logic. The
if (result.isAccepted())is coordination (deciding the flow); anif (customer.isVip()) discount = ...would be business (deciding rules) and belongs in the subsystem. The line is thin: ask whether the rule would also make sense invoked from another flow — if yes, down it goes. - Not testing the facade as a flow. The facade is the natural home for orchestration tests: with doubles of the five services, verify the ordering, the cut-off when payment fails, the no-notification on error. If those tests don't exist, the business's most delicate flow has no safety net.
- Tip: design the facade's interface from the client, not from the subsystem: first write how you want the app code to read (
confirmOrder(cart, payment)), then make the facade honor it. Facades designed from the inside end up smelling of the guts they were meant to hide.
Exercises
Exercise 1: the tracking facade
The "where is my order?" screen needs: the kitchen ticket status (KitchenService.statusOf(orderNumber)), the courier's position and name if already out (DeliveryService.deliveryOf(orderNumber)), and the recalculated estimated time (EtaCalculator.estimate(ticket, delivery)). Today the three calls and their combination are copied into iOS, Android, and web. Design TrackingFacade: public signature, a return type owned by the boundary, and an implementation skeleton.
Exercise 2: the impostor facade
What two violations of the pattern does this version contain, and why are they dangerous?
public class CheckoutFacade {
private int ordersConfirmedToday = 0; // (a)
public OrderConfirmation confirmOrder(Cart cart, PaymentData data) {
if (cart.getCustomer().isVip() && ordersConfirmedToday < 100) {
cart.applyDiscount(new BigDecimal("0.05")); // (b)
}
// ... rest of the flow as before ...
ordersConfirmedToday++;
// ...
}
}Exercise 3: direct access or extend the facade?
The accounting team needs, for its daily close, the day's list of payment transactions exactly as the gateway returns them. Discuss: should they request it through CheckoutFacade or go straight to the payments subsystem? Give a reusable general criterion.
Solutions
Solution 1:
public class TrackingFacade {
private final KitchenService kitchen;
private final DeliveryService delivery;
private final EtaCalculator eta;
public TrackingFacade(KitchenService kitchen, DeliveryService delivery, EtaCalculator eta) {
this.kitchen = kitchen; this.delivery = delivery; this.eta = eta;
}
/** A type owned by the boundary: only what the screen needs. */
public record CustomerOrderStatus(String phase, String courierName,
Position courierPosition, LocalTime estimatedTime) {}
public CustomerOrderStatus statusOf(OrderNumber number) {
TicketStatus ticket = kitchen.statusOf(number);
Optional<Delivery> inProgress = delivery.deliveryOf(number);
LocalTime estimated = eta.estimate(ticket, inProgress.orElse(null));
return new CustomerOrderStatus(
ticket.readablePhase(),
inProgress.map(Delivery::getCourierName).orElse(null),
inProgress.map(Delivery::getPosition).orElse(null),
estimated);
}
}Key points: one single call for the three clients, a return type of its own (CustomerOrderStatus, a boundary record) instead of leaking TicketStatus/Delivery, and the combination (what happens when there is no courier yet) written once.
Solution 2: (a) State of its own: ordersConfirmedToday makes the facade the owner of a business datum nobody else sees — lost on restart, broken with multiple instances, and duplicating a truth that belongs to the repository. (b) Smuggled business logic: the VIP discount rule lives where nobody will look for it (will the promotions team find it when the policy changes?) and is invisible to any other flow that confirms orders (the partner API wouldn't apply it: inconsistent pricing). Both also make the facade hard to replace or test, which was its whole point: it must go back to pure coordination, and the rules must move down into the subsystem (promotions service, repository).
Solution 3: straight to the payments subsystem (or better, to a facade of the payments area if one exists). Reusable criterion: the facade serves the common use cases of its clients, at their level of abstraction; accounting is asking for raw data from one specific subsystem, in that subsystem's vocabulary, for a use case foreign to the checkout. Extending CheckoutFacade with todaysTransactions() would drift it from its purpose and fatten it up (the "facade that fattens up" mistake). The pattern doesn't forbid direct access: use it when the use case doesn't belong to the facade.
Conclusion
Facade puts a simple door in front of a complex subsystem: one class offering the high-level operations clients really need, writing the delicate flow exactly once, and reducing coupling from a graph to a funnel. You also know what it is not: not a ban on direct access, not a state store, not a coat rack for business logic. In PideYa, CheckoutFacade brought together validation, per-market taxes, charging, order construction, and notification — all reusing pieces from this module and the previous one, which is the best sign that the system is well composed.
We now change concern entirely: from order to scale. PideYa's real-time map draws thousands of couriers and restaurants, and every marker drags along its icon, its bytes, its memory... multiplied by ten thousand. When objects come in crowds, the structural question is different: how much of what each object carries is truly its own, and how much could be shared among all of them? See you in Flyweight.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What Are Design Patterns?
- History and Origin of Design Patterns
- Design Principles: SOLID and Other Foundations
- Essential UML for Understanding Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
- Introduction to Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparing and Choosing Creational Patterns
Module 3: Structural Patterns
- Introduction to Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparing and Choosing Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparing and Choosing Behavioral Patterns
Module 5: Applying Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring with Design Patterns
- Anti-Patterns: When Patterns Become a Problem
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Concurrency Patterns
- Design Patterns in Agile Development
