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
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
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.