Design Patterns And Data Architecture

Facade Pattern

A facade provides a simpler interface over a more complex subsystem. In PHP applications, facades appear around reporting workflows, payment subsystems, search indexing, file generation, notification stacks, framework services, and legacy modules. The facade gives callers one clear entry point while hiding coordination details that callers should not repeat.

A facade is useful when a feature requires several collaborators in a fixed sequence. For example, generating a monthly report might load data, check permissions, render a file, store it, record an audit entry, and return a download reference. If every controller repeats that sequence, the workflow will drift. A facade can expose generateMonthlyReport() and keep the internal subsystem behind one application-owned boundary.

The pattern is not a license to create a god object. A facade should simplify access to a cohesive subsystem. If one class named ApplicationFacade knows users, invoices, payments, search, cache, mail, and deployment, it has become a broad service locator or manager. Good facades are narrow.

What A Facade Owns

A facade owns a simplified API over several internal objects. It may coordinate repositories, builders, renderers, adapters, policies, transaction managers, and event recorders. It should expose operations that make sense to the caller.

The caller should not need to know which five classes must be called or in which order. The facade should not hide important outcomes, though. If the workflow can fail because of permission, validation, missing data, temporary storage outage, or rendering error, callers need outcomes they can act on.

A facade is often an application-layer boundary. It can sit above lower-level patterns such as repositories, adapters, decorators, and builders.

A Small Facade Example

This example coordinates two small collaborators behind one report facade.

PHP example
<?php

declare(strict_types=1);

final class ReportDataLoader
{
    public function load(int $accountId): string
    {
        return 'data for account ' . $accountId;
    }
}

final class ReportRenderer
{
    public function render(string $data): string
    {
        return 'REPORT: ' . strtoupper($data);
    }
}

final class ReportFacade
{
    public function __construct(
        private ReportDataLoader $loader,
        private ReportRenderer $renderer,
    ) {
    }

    public function generateForAccount(int $accountId): string
    {
        $data = $this->loader->load($accountId);

        return $this->renderer->render($data);
    }
}

$facade = new ReportFacade(new ReportDataLoader(), new ReportRenderer());

echo $facade->generateForAccount(42) . PHP_EOL;

// Prints:
// REPORT: DATA FOR ACCOUNT 42

The caller asks for a report. It does not know how data loading and rendering are split internally. The facade is valuable if that split is an implementation detail the caller should not repeat.

Facade Versus Adapter

A facade simplifies a subsystem. An adapter translates one interface into another. They can look similar because both hide details behind a new interface, but their motivation differs.

Use an adapter when the problem is shape mismatch with a dependency, provider, framework service, or legacy API. Use a facade when the problem is that callers should not coordinate several internal collaborators themselves.

For example, StripePaymentAdapter translates the Stripe SDK into your PaymentAuthorizer interface. CheckoutPaymentFacade might coordinate idempotency, payment authorization, order state, outbox events, and audit logging. The adapter hides provider shape; the facade hides subsystem orchestration.

Facade Versus Service

Facades and services can overlap. A narrow application service that coordinates several collaborators may effectively be a facade for one use case. The distinction matters mainly when the class is presented as a stable simplified API over a subsystem.

A facade often groups a small set of related operations. A ReportFacade might offer generateMonthly(), generateCsv(), and downloadUrl() if those operations are part of the reporting subsystem. An action would usually represent one operation. A service may be either broad or narrow depending on codebase convention.

Choosing Facade Granularity

Facade size should follow the way the product is understood and operated. A reporting facade can reasonably group report generation, report storage lookup, and report download references because those operations share language, collaborators, and failure modes. It should not also send marketing email or rebuild search indexes just because those features happen near reports in the UI.

A useful test is whether an on-call developer would investigate the facade's operations through the same logs, dashboards, queues, and runbook. If methods require unrelated operational evidence, they probably belong behind separate facades or actions. Cohesion is not only a code concern; it affects incident diagnosis.

Do not argue about the label more than the responsibility. Ask whether the class simplifies a cohesive subsystem and whether its public methods are meaningful.

Framework Facades

Some PHP frameworks use the word facade for static-looking access to services. Laravel facades are the most familiar example. They provide a convenient interface to container-managed services. That framework feature is related in spirit but has specific mechanics and testing conventions.

This lesson focuses on the design pattern, not one framework's facade implementation. When using a framework facade, follow the framework's documentation and testing tools. When designing your own facade, keep dependencies explicit unless the framework convention intentionally manages them.

Static access can be convenient, but it can also hide dependencies. Application code that calls global facades everywhere may be harder to test and reason about than code with explicit constructor dependencies.

Keeping Facades Narrow

A facade should have a clear subsystem boundary. Good names include ReportGeneration, InvoiceExport, SearchIndexing, ReceiptDelivery, or AccountProvisioning. Weak names include SystemFacade, AppFacade, Manager, and Helper.

If a facade's constructor needs many unrelated dependencies, split it. If its methods do not share collaborators or language, split it. If callers use only one method and the rest are unrelated, the facade is probably too broad.

The facade should reduce what callers need to know, not become the only place anyone can understand the system.

Error Handling

Facades should translate internal failures into useful application outcomes. They should not catch everything and return false. If the report cannot be generated because the account is missing, permission is denied, storage is unavailable, or rendering failed, those cases matter.

A facade may wrap lower-level exceptions with a subsystem-specific exception. It may return result objects for expected business outcomes. The choice should match how callers recover.

Log internal context carefully. A facade can add operation name and correlation ID, but it should not expose secrets or raw provider payloads in public messages.

Transactions And Side Effects

Facades often coordinate operations that cross persistence and external side effects. That makes ordering important. If a facade saves a database record, writes a file, and publishes an event, reviewers need to know which step is authoritative and how failures recover.

Do not hide transaction boundaries so deeply that callers cannot reason about consistency. If the facade owns the workflow, it can own the transaction or call a use case that does. If a caller already owns a broader transaction, a facade that commits internally may be dangerous.

Facades also create compatibility boundaries inside a codebase. Once several controllers, jobs, or commands depend on a facade method, changing its result shape or failure behavior is a breaking internal API change. Make changes additively where possible, and update all delivery mechanisms together when the operation contract changes.

Document whether facade operations are idempotent, retryable, and safe to call from queues.

Testing Facades

Facade tests should prove orchestration. Use fakes for internal collaborators and assert the facade calls them in the intended order when order matters. Test success and important failure cases.

Do not mock so much that the test merely repeats implementation. Choose assertions around observable outcomes: report reference created, storage called once, event recorded after successful storage, permission failure stops rendering.

Lower-level collaborators still need their own tests. A facade test proving that a renderer was called does not prove rendering is correct.

Add observability at the facade boundary when the subsystem is operationally important. A report facade can log report type, actor, account, duration, result category, and generated file reference. Use bounded identifiers and safe codes, not raw document contents or secret-bearing provider payloads. This gives support and operations one place to trace the workflow without coupling controllers to internal steps.

Treat that trace as part of the facade contract.

Common Failure Modes

One failure mode is the god facade. It becomes a single entry point for unrelated application behavior and hides all dependencies.

Another failure mode is a facade that suppresses meaningful errors. Simplifying the interface should not erase recovery information.

A third failure mode is using a facade to avoid learning the subsystem. If nobody understands what happens behind it, debugging becomes harder.

A fourth failure mode is making a facade too thin. If it only calls one method with the same arguments and no naming or sequencing value, the facade may be unnecessary.

Review Criteria

When reviewing a facade, ask what subsystem it simplifies and which details it hides from callers. Check that public methods use application language, not internal class names. Check that it remains cohesive.

Check errors, transactions, side effects, retry behavior, and observability. Check that tests cover orchestration and failure paths. Check whether explicit dependencies would be clearer than static access.

What To Check

Before moving on, make sure you can:

  • explain facade as a simplified interface over a cohesive subsystem
  • distinguish facades from adapters, services, actions, and framework facades
  • keep facades narrow and application-owned
  • preserve meaningful error outcomes
  • make transaction and side-effect ordering visible
  • test facade orchestration without pretending lower-level collaborators are tested
  • spot god facades and unnecessary pass-through facades

After this lesson, you should be able to decide whether a PHP feature needs a facade because callers are coordinating too much, or whether a smaller action, service, adapter, or direct dependency is clearer.

Practice

Task: Design A Report Facade

A controller currently loads report data, checks permission, renders CSV, stores the file, records audit, and returns a download reference.

Design a facade for this subsystem.

Requirements

Name the facade, public method, collaborators, result, and failure categories.

Check your work

The controller should not repeat the internal workflow.

Show solution

The facade checks permission, loads data, renders the CSV, stores it, records audit, and returns a download reference. Failure categories should distinguish forbidden, missing account, render failure, storage failure, and unexpected internal failure.

The controller reads HTTP input, calls the facade, and maps the result or failure to an HTTP response.

Task: Review Facade Overreach

A class named ApplicationFacade has methods for user registration, invoice export, search indexing, cache clearing, payment capture, and password reset.

Review the design.

Requirements

Explain why it is too broad and propose better boundaries.

Check your work

The answer should split by cohesive subsystem, not rename it to another broad manager.

Show solution

ApplicationFacade is too broad because its methods do not describe one cohesive subsystem. It will gather unrelated dependencies and become a service locator or god object.

Split by subsystem or use case. Registration and password reset may be separate actions or account services. Invoice export can have an InvoiceExportFacade. Search indexing can have SearchIndexing. Payment capture should live behind a payment use case or facade with idempotency and provider boundaries.

The replacement should make each public method belong to the same vocabulary and collaborator set.

Task: Test A Facade Boundary

An InvoiceExportFacade loads invoice data, renders PDF, stores the file, and records an outbox event.

Plan facade tests.

Requirements

Include success, missing invoice, render failure, storage failure, event ordering, and what lower-level tests remain separate.

Check your work

The facade test should prove orchestration, not PDF rendering internals.

Show solution

Use fakes for repository, renderer, storage, and outbox. The success test should assert data is loaded, PDF render is requested, storage receives the rendered bytes, and the outbox event is recorded only after storage succeeds.

Missing invoice should stop rendering and storage. Render failure should stop storage and event recording. Storage failure should stop event recording and return or throw the documented failure.

Separate renderer tests should prove PDF content. Separate storage tests should prove provider integration. The facade test proves workflow ordering and failure behavior.