← All patterns

Facade

Structural · view source on GitHub

Read this in: English · Português · Español

Category: Structural

The problem

Getting something done requires coordinating several subsystems in a specific order — call this service, then that one, only proceed if each step succeeds. Every caller that needs this outcome either duplicates that orchestration logic, or has to learn the internals of every subsystem just to use them correctly. The subsystems themselves are fine on their own; what's missing is a simpler front door for the common case.

The solution

Add one class that knows how to coordinate the subsystems correctly, and give callers that instead of the subsystems themselves. The subsystems don't change and stay usable directly for callers with more specific needs — the facade is an additional simpler entry point, not a replacement.

classDiagram
    class Facade {
        +operation()
    }
    class SubsystemA
    class SubsystemB
    class SubsystemC
    Facade --> SubsystemA
    Facade --> SubsystemB
    Facade --> SubsystemC
    Client --> Facade

Classic example

classic/HomeTheaterFacade is the canonical example: watchMovie() powers on the Projector, sets it to widescreen, powers on the Amplifier and sets its volume, then powers on the DvdPlayer and starts the movie — six calls across three subsystems, in the one order that actually works, behind one method. HomeTheaterFacadeTest asserts the exact sequence.

Applied example: salary portability orchestration

applied/SalaryPortabilityFacade coordinates AccountVerificationService (is this account even eligible), BacenLookupService (where is this payer's salary currently paid, per the central bank's registry), and NotificationService (tell the account holder it's scheduled) — short-circuiting the moment any step fails, so an ineligible account never triggers a BACEN lookup, and a payer with no registered payroll bank never triggers a notification. None of the three subsystem services know the other two exist; only the facade does. SalaryPortabilityFacadeTest covers the full happy path and both short-circuit cases.

When not to use it

Test coverage

100% instruction coverage, 100% branch coverage (JaCoCo). Reproduce it yourself:

./gradlew :structural:facade:jacocoTestReport

Report at structural/facade/build/reports/jacoco/test/html/index.html.

Further reading

Unit tests

src/test/java/com/designpatterns/structural/facade/classic/HomeTheaterFacadeTest.java
package com.designpatterns.structural.facade.classic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class HomeTheaterFacadeTest {

    @Test
    void watchMovieOrchestratesEverySubsystemInTheRightOrder() {
        HomeTheaterFacade homeTheater = new HomeTheaterFacade(new Amplifier(), new DvdPlayer(), new Projector());

        var log = homeTheater.watchMovie("The Matrix");

        assertThat(log).containsExactly(
                "Projector on",
                "Projector in widescreen mode",
                "Amplifier on",
                "Amplifier volume set to 5",
                "DVD player on",
                "Playing \"The Matrix\""
        );
    }
}
src/test/java/com/designpatterns/structural/facade/applied/SalaryPortabilityFacadeTest.java
package com.designpatterns.structural.facade.applied;

import org.junit.jupiter.api.Test;

import java.util.Map;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;

class SalaryPortabilityFacadeTest {

    private final AccountVerificationService verification = new AccountVerificationService(Set.of("acc-1"));
    private final BacenLookupService bacenLookup = new BacenLookupService(Map.of("111.111.111-11", "Bank A"));
    private final SalaryPortabilityFacade facade =
            new SalaryPortabilityFacade(verification, bacenLookup, new NotificationService());

    @Test
    void schedulesPortabilityWhenEverySubsystemAgrees() {
        PortabilityResult result = facade.requestPortability("acc-1", "111.111.111-11");

        assertThat(result.scheduled()).isTrue();
        assertThat(result.fromBank()).isEqualTo("Bank A");
        assertThat(result.message()).isEqualTo("Notice sent to account acc-1: portability from Bank A scheduled");
    }

    @Test
    void rejectsAnIneligibleAccountWithoutEverCallingBacen() {
        PortabilityResult result = facade.requestPortability("acc-unknown", "111.111.111-11");

        assertThat(result.scheduled()).isFalse();
        assertThat(result.message()).isEqualTo("account not eligible for portability");
    }

    @Test
    void rejectsWhenBacenHasNoPayrollRegistrationForTheTaxId() {
        PortabilityResult result = facade.requestPortability("acc-1", "999.999.999-99");

        assertThat(result.scheduled()).isFalse();
        assertThat(result.message()).isEqualTo("no payroll registration found at BACEN");
    }
}

View full JaCoCo coverage report →