jonnifyelixir
F6 / F6.04 / vs-facade

F6.04 · part 2 of 3

Contexts vs the F5 facade

Two communities arrived at the same boundary from different directions, and it is worth naming the overlap precisely. Phoenix calls it a context: a module that groups related functionality and exposes a public API. The hexagonal architecture this course built in F5 calls the same thing a facade — the Portal module the web layer is allowed to call, and nothing deeper. They are the same shape: one public surface over a slice of the domain. The differences are emphasis. Phoenix’s default context talks to Ecto directly, so the context is the persistence boundary. The F5 design adds one more seam — a port, the Portal.EventStore behaviour from F5.09 — so the context depends on an abstraction and Ecto lives in an adapter behind it (F6.03). This dive lines the three terms up, shows how they layer in a real application, and resolves the one question that trips people moving between the two worlds: where does Ecto go?

Four parts: the three terms side by side, how a real app layers web over facade over contexts over adapters, the facade delegating in code, and where Ecto sits once a port is in play.

Three terms, one boundary

The same wall has three names depending on who is describing it. Context is Phoenix’s word for the public-API module. Facade is the F5 word for the one entry the web layer calls. Port is the deeper seam — a behaviour a context depends on so its adapter can be swapped. Select a term to see what it emphasises.

One boundary, three terms · select one

CONTEXT Phoenix: a module that groups functionality + exposes an API framework word FACADE F5: the one public entry the web layer calls (Portal) same boundary PORT a behaviour a context depends on, so its adapter can swap deeper seam

term: Facade

is: F5's name for it

Do not let the vocabulary multiply the concept. If a teammate says “context” and the F5 docs say “facade,” they mean the same module — a public API over a slice. The port is the only genuinely extra idea, and it is optional.

How they layer

In a running application the terms stack. The web layer calls the Portal facade; the facade delegates to contexts (Catalog, Enrollment); each context depends on a port; an adapter implements the port with Ecto or the in-memory store. Read top to bottom, every layer talks only to the one below it, and the database is four steps removed from a controller.

web → facade → contexts → adapters

WEB · controllers & LiveViews Portal — the facade (one public entry) Catalog (context) Enrollment (context) Portal.EventStore — the port (a behaviour) adapter: Postgres (Ecto) | InMemory — chosen by config

Phoenix’s default collapses the bottom two layers — the context calls Ecto directly. The F5 design keeps them apart with a port, which is what made the in-memory adapter for tests and the Postgres adapter for production interchangeable in F6.03.

The facade delegates

The Portal facade is deliberately thin: it names the contexts and forwards to them, so the web layer has one module to import and the slices stay separate behind it. defdelegate is the idiomatic way to forward a function to another module without re-implementing it — the facade becomes a curated index of the application’s public API.

defmodule Portal do
  @moduledoc "The application facade. The web layer calls Portal and nothing deeper."

  alias Portal.{Catalog, Enrollment, Accounts}

  # catalog
  defdelegate list_courses(),               to: Catalog
  defdelegate get_course!(id),              to: Catalog

  # enrollment
  defdelegate enroll(user_id, course_id),    to: Enrollment
  defdelegate courses_of(user_id),          to: Enrollment

  # accounts
  defdelegate get_user(id),                 to: Accounts
end

The facade owns no logic; each context does. That keeps the single entry point convenient for the web layer without turning Portal into a god module — it is a table of contents, not a chapter.

Where Ecto goes

This is the question that separates the two styles. In a stock Phoenix app the context calls Repo directly — perfectly fine, and what most apps do. In this course the context depends on the Portal.EventStore port instead, and Ecto lives in the Postgres adapter behind it (F6.03). The context below reads and appends through the port; it never names Repo, so the same Enrollment works against the in-memory adapter in a test and Postgres in production with no change.

defmodule Portal.Enrollment do
  @moduledoc "Enrollment: who is enrolled in what, and their progress."

  alias Portal.EventStore           # the F5.09 port — not Repo
  alias Portal.Enrollment.Enrolled

  def enroll(user_id, course_id) do
    event = %Enrolled{user_id: user_id, course_id: course_id, at: DateTime.utc_now()}
    EventStore.append("enrollment:#{user_id}", [event])
  end

  def courses_of(user_id) do
    {:ok, events} = EventStore.read_stream("enrollment:#{user_id}")
    events |> Enum.map(fn e -> e.course_id end) |> Enum.uniq()
  end
end

stock Phoenix

The context calls Repo directly — the context is the persistence edge.

this course

The context calls a port; Ecto lives in the adapter behind it (F6.03).

Next: composing contexts — Enrollment needs to know a course exists, so it must call Catalog without reaching into it.