F5.02 · part 3 of 3
A context's public API
A context is only as decoupled as its surface is small. Each one exposes a handful of
public functions — a smart constructor that validates and returns
{:ok, struct} or {:error, reason}, a command or two, a query
— and keeps its structs, validations, and helpers defp-private. Callers depend on
the API, not the data, so the context can rearrange its internals freely. This is the contract every caller
leans on: the thin server today, Phoenix in F6, and the tests in between.
The public functions of the Learning context, and what each one promises. Select a function to see its shape.
Public surface, private core
Everything above the line is callable; everything below it is not. A command returns a tagged result so the caller can branch; a query returns data with no side effects; a smart constructor refuses to build an invalid entity in the first place. The structs and the validation that backs those promises stay private. Select a function on the surface.
The Learning API · select a function
function: enroll/2
kind: command (constructor)
The API is the contract. Keep it small and tagged, keep the structs and validation private, and a caller can never put the context into a state its rules forbid.
In code
The public functions carry @specs and return tagged tuples; the smart constructor
runs validation through a with chain and only then builds the struct; the validation
itself is defp. A caller sees three functions and three contracts, not the data
behind them.
defmodule Portal.Learning do alias Portal.Learning.Enrollment @spec enroll(String.t(), String.t()) :: {:ok, Enrollment.t()} | {:error, atom()} def enroll(user_id, course_id) do with :ok <- validate_ids(user_id, course_id) do {:ok, %Enrollment{id: Portal.ID.new("ENR"), user_id: user_id, course_id: course_id}} end end @spec courses_of(String.t()) :: [Enrollment.t()] def courses_of(user_id), do: # a query — no side effects # private — not part of the API surface defp validate_ids("USR" <> _ = _u, "CRS" <> _ = _c), do: :ok defp validate_ids(_, _), do: {:error, :bad_reference} end
a small surface
A few public functions, each with a contract — the only way into the context.
a hidden core
Structs and defp validation stay private, free to change.
That closes F5.02: the Portal’s entities are typed structs, grouped into decoupled contexts, each behind a small API. The next module, F5.03 — Tracer bullets, drives one use case through this model end to end. Back to the module overview or the chapter.
References
Primary sources for this lesson, and where it connects in the course.
Sources
ExUnit— Elixir documentation — the test framework that exercises a context's API.ExUnit.DocTest— Elixir documentation — tests built from documentation examples.