Skip to content
3 min read

🏛 Facade Design Pattern

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.


💡 Real-World Analogy

Think of a Hotel Concierge

When you stay at a hotel, you don't call the kitchen, housekeeping, taxi service, and restaurant separately. You call the concierge — one person who coordinates everything for you. The concierge is the Facade. They hide the complexity of multiple subsystems behind a single, friendly interface.

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
    You["🧳 You"] -->|"asks"| C{{"🛎️ Concierge"}}
    C -->|"calls"| K["👨‍🍳 Kitchen"]
    C -->|"calls"| T["🚕 Taxi"]
    C -->|"calls"| S["🧖 Spa"]

    style You fill:#E8F5E9,stroke:#2E7D32,color:#000
    style C fill:#FFF3E0,stroke:#E65100,stroke-width:2px,color:#000
    style K fill:#E3F2FD,stroke:#1565C0,color:#000
    style T fill:#E3F2FD,stroke:#1565C0,color:#000
    style S fill:#E3F2FD,stroke:#1565C0,color:#000

📐 Pattern Structure

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
    Client["🖥️ Client"] -->|"simple API"| Facade{{"🏛️ Facade"}}
    Facade -->|"orchestrates"| SubA(["⚙️ Subsystem A"])
    Facade -->|"orchestrates"| SubB(["⚙️ Subsystem B"])
    SubA --> SubC(["⚙️ Subsystem C"])

    style Client fill:#E8F5E9,stroke:#2E7D32,color:#000
    style Facade fill:#FFF3E0,stroke:#E65100,stroke-width:2px,color:#000
    style SubA fill:#E3F2FD,stroke:#1565C0,color:#000
    style SubB fill:#E3F2FD,stroke:#1565C0,color:#000
    style SubC fill:#E3F2FD,stroke:#1565C0,color:#000

UML Class Diagram

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
classDiagram
    class OrderFacade {
        -inventory: InventoryService
        -payment: PaymentService
        -shipping: ShippingService
        -notification: NotificationService
        +placeOrder(userId, productId, quantity, address, email) String
    }
    class InventoryService {
        +checkStock(productId, quantity) boolean
        +reserveStock(productId, quantity) void
    }
    class PaymentService {
        +processPayment(userId, amount) boolean
        +refund(transactionId) void
    }
    class ShippingService {
        +createShipment(orderId, address) String
    }
    class NotificationService {
        +sendOrderConfirmation(email, orderId) void
        +sendShippingNotification(email, trackingId) void
    }
    class Client {
        +main(args) void
    }

    Client --> OrderFacade : uses
    OrderFacade *-- InventoryService
    OrderFacade *-- PaymentService
    OrderFacade *-- ShippingService
    OrderFacade *-- NotificationService

❌ The Problem

Imagine placing an online order. Behind the scenes, the system must:

  1. Verify inventory
  2. Process payment
  3. Update order database
  4. Send confirmation email
  5. Notify shipping service

Without a facade, the client code must know about and interact with 5 different subsystems, understand their APIs, handle their exceptions, and coordinate the workflow. This creates:

  • Tight coupling between client and subsystems
  • Fragile code — any subsystem change breaks the client
  • Duplicated orchestration logic across multiple clients

Without This Pattern

Java
// BAD: Every client must know about and coordinate 5 subsystems
public class CheckoutController {

    public void handleCheckout(String userId, String productId, int qty, String address) {
        // Client is forced to know the internal orchestration order
        InventoryService inventory = new InventoryService();
        PaymentService payment = new PaymentService();
        ShippingService shipping = new ShippingService();
        NotificationService notification = new NotificationService();
        OrderDatabase orderDb = new OrderDatabase();

        if (!inventory.checkStock(productId, qty)) {
            throw new RuntimeException("Out of stock");
        }
        inventory.reserveStock(productId, qty);
        boolean paid = payment.charge(userId, 29.99 * qty);
        if (!paid) {
            inventory.releaseStock(productId, qty); // manual rollback!
            throw new RuntimeException("Payment failed");
        }
        String orderId = orderDb.createOrder(userId, productId, qty);
        String trackingId = shipping.createShipment(orderId, address);
        notification.sendEmail(userId, "Order " + orderId + " confirmed");
        notification.sendSms(userId, "Tracking: " + trackingId);
    }
}

// Another controller duplicates the SAME orchestration logic!
public class MobileCheckoutController {
    public void checkout(/* same parameters */) {
        // Copy-pasted orchestration — any bug fix must be applied in BOTH places
    }
}

Problems:

  • Tight coupling to subsystems: Every client (web controller, mobile controller, batch job) directly depends on 5+ internal services — a change in any subsystem API breaks all clients
  • Duplicated orchestration logic: The same multi-step workflow is copy-pasted across controllers — bug fixes and order changes must be applied in multiple places
  • Fragile error handling: Manual rollback logic (release stock if payment fails) is the client's responsibility — easy to forget and introduce inconsistencies
  • Violates Law of Demeter: The controller "reaches through" multiple layers, knowing intimate details about inventory, payment, and shipping APIs
  • Pain point: A new developer joins and needs to build an admin "place order on behalf of customer" feature — they must reverse-engineer the exact 7-step orchestration sequence from existing code, hoping they get the order and error handling right

✅ The Solution

The Facade pattern provides a single entry point that orchestrates multiple subsystem operations. Clients interact with one simple method; the facade handles all the complexity internally.

Key characteristics:

  • Facade doesn't add new functionality — it simplifies access to existing functionality
  • Subsystems are still accessible directly if needed (facade doesn't lock you in)
  • Multiple facades can exist for different use cases

🛠 Implementation

Java
// Subsystem 1: Inventory
public class InventoryService {
    public boolean checkStock(String productId, int quantity) {
        System.out.println("Checking inventory for: " + productId);
        return true; // simplified
    }

    public void reserveStock(String productId, int quantity) {
        System.out.println("Reserved " + quantity + " units of " + productId);
    }
}

// Subsystem 2: Payment
public class PaymentService {
    public boolean processPayment(String userId, double amount) {
        System.out.println("Processing payment of $" + amount + " for user: " + userId);
        return true;
    }

    public void refund(String transactionId) {
        System.out.println("Refunding transaction: " + transactionId);
    }
}

// Subsystem 3: Shipping
public class ShippingService {
    public String createShipment(String orderId, String address) {
        System.out.println("Creating shipment for order: " + orderId);
        return "TRACK-" + orderId;
    }
}

// Subsystem 4: Notification
public class NotificationService {
    public void sendOrderConfirmation(String email, String orderId) {
        System.out.println("Sending confirmation to " + email + " for order " + orderId);
    }

    public void sendShippingNotification(String email, String trackingId) {
        System.out.println("Sending tracking info: " + trackingId);
    }
}

// FACADE — the simple interface
public class OrderFacade {

    private final InventoryService inventory;
    private final PaymentService payment;
    private final ShippingService shipping;
    private final NotificationService notification;

    public OrderFacade() {
        this.inventory = new InventoryService();
        this.payment = new PaymentService();
        this.shipping = new ShippingService();
        this.notification = new NotificationService();
    }

    /**
     * Single method to place an entire order.
     * Client doesn't need to know about subsystems.
     */
    public String placeOrder(String userId, String productId,
                             int quantity, String address, String email) {

        // Step 1: Check inventory
        if (!inventory.checkStock(productId, quantity)) {
            throw new RuntimeException("Product out of stock");
        }
        inventory.reserveStock(productId, quantity);

        // Step 2: Process payment
        double amount = calculateTotal(productId, quantity);
        if (!payment.processPayment(userId, amount)) {
            throw new RuntimeException("Payment failed");
        }

        // Step 3: Create shipment
        String orderId = generateOrderId();
        String trackingId = shipping.createShipment(orderId, address);

        // Step 4: Notify customer
        notification.sendOrderConfirmation(email, orderId);
        notification.sendShippingNotification(email, trackingId);

        return orderId;
    }

    private double calculateTotal(String productId, int quantity) {
        return 29.99 * quantity; // simplified
    }

    private String generateOrderId() {
        return "ORD-" + System.currentTimeMillis();
    }
}

// Client code — simple!
public class Client {
    public static void main(String[] args) {
        OrderFacade facade = new OrderFacade();
        String orderId = facade.placeOrder(
            "user123", "LAPTOP-001", 1,
            "123 Main St", "user@email.com"
        );
        System.out.println("Order placed: " + orderId);
    }
}
Java
@Service
public class UserRegistrationFacade {

    private final UserService userService;
    private final EmailService emailService;
    private final WalletService walletService;
    private final AnalyticsService analyticsService;

    public UserRegistrationFacade(UserService userService,
                                   EmailService emailService,
                                   WalletService walletService,
                                   AnalyticsService analyticsService) {
        this.userService = userService;
        this.emailService = emailService;
        this.walletService = walletService;
        this.analyticsService = analyticsService;
    }

    @Transactional
    public UserDto registerUser(RegistrationRequest request) {
        // Orchestrate multiple subsystems
        User user = userService.createUser(request);
        walletService.createWallet(user.getId(), BigDecimal.ZERO);
        emailService.sendWelcomeEmail(user.getEmail(), user.getName());
        analyticsService.trackEvent("USER_REGISTERED", user.getId());

        return UserDto.from(user);
    }

    public void deactivateUser(Long userId) {
        userService.deactivate(userId);
        walletService.freezeWallet(userId);
        emailService.sendDeactivationEmail(userId);
        analyticsService.trackEvent("USER_DEACTIVATED", userId);
    }
}

// Controller uses the facade — clean and simple
@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserRegistrationFacade registrationFacade;

    @PostMapping("/register")
    public ResponseEntity<UserDto> register(@RequestBody RegistrationRequest request) {
        UserDto user = registrationFacade.registerUser(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
}

🎯 When to Use

  • You need a simple interface to a complex subsystem
  • There are many dependencies between clients and implementation classes
  • You want to layer your subsystems — facade as entry point to each layer
  • You want to decouple client code from subsystem internals
  • You're building an API layer over complex business logic

🌐 Real-World Examples

Where Example
Spring JdbcTemplate — facades over raw JDBC (Connection, Statement, ResultSet)
Spring RestTemplate / WebClient — facades over HTTP handling
Spring Boot Auto-configuration is a facade over complex bean wiring
JDK java.net.URL — facades over socket connections, HTTP protocol
SLF4J Facade over multiple logging frameworks
Hibernate Session — facades over SQL, connection pooling, caching
AWS SDK High-level TransferManager facades over S3 multipart upload API

⚠ Pitfalls

Common Mistakes

  • God Facade: A facade that does too much becomes a "god object" — split into multiple focused facades
  • Forcing exclusive access: Don't prevent clients from accessing subsystems directly when they need fine-grained control
  • Business logic in facade: Facade should orchestrate, not contain business logic — that belongs in services
  • Tight coupling within facade: Use dependency injection rather than instantiating subsystems directly
  • Confusing with Adapter: Facade simplifies; Adapter converts. Facade wraps many classes; Adapter wraps one

📝 Key Takeaways

Summary

Aspect Detail
Intent Simplify access to a complex subsystem
Mechanism Single unified interface that delegates to subsystems
Key Benefit Reduces coupling; provides a clean API
Key Principle Least Knowledge (Law of Demeter)
Does NOT Add new functionality — only simplifies existing APIs
Interview Tip "Spring's JdbcTemplate is the classic Facade — it hides Connection, Statement, and ResultSet management"