F6.07 · part 1 of 3
Broadcasting engine events
Phoenix.PubSub is a publish/subscribe registry running as a named
process tree in your application. You publish with
broadcast(server, topic, message) and it delivers that message to every process
subscribed to the topic — a plain string like "courses". The
message is any term; the convention is a tagged tuple naming a domain event,
{:course_created, course}, so a handler can pattern-match it. The design question is
not how to call broadcast — it is one line — but where. The answer follows the F6.04
boundary: the context broadcasts, right after a successful write, because creating a course is a
domain fact and the rest of the system should hear about it regardless of who triggered it. The web layer never
broadcasts; it would couple the domain’s events to one delivery path. A small piece of plumbing keeps this
clean: a broadcast/2 helper that fires only on {:ok, _} and
passes errors through untouched, and a facade wrapper so the server name and the topics live in
one place rather than scattered through the codebase.
Four parts: the three pieces of a broadcast, where it belongs in the layers, broadcasting from a context in code, and the facade wrapper plus supervision setup.
Server, topic, message
A broadcast names three things. The PubSub server is the named registry process that routes
messages. The topic is the string channel a message travels on.
broadcast/3 is the call that sends a message to everyone subscribed. Select one.
A broadcast · select one
piece: broadcast/3
does: send a message to all subscribers
A topic is only an agreed string, so naming it is a design choice. Per-resource topics like
"course:" <> id let a subscriber listen to one record; a broad
"courses" topic carries every change to the collection.
Where the broadcast belongs
The broadcast lives in the context, immediately after the write, for the same reason the
context owns the write at all: it is the domain’s job to say what happened. A controller or LiveView
that broadcast directly would tie the event to one caller and one transport. Put it in
create_course/1 and every path — web form, live form, an import script —
emits the same event for free.
caller → context (writes & broadcasts) → PubSub
This mirrors F5 exactly: the engine recorded an event when a fact occurred, independent of any caller. PubSub is the live delivery of that same idea — the event is born in the domain and travels outward.
Broadcasting from a context
create_course/1 pipes the insert result into a private
broadcast/2 helper. The helper fires the broadcast only on success and returns the
result unchanged, so the function’s contract — {:ok, course} or
{:error, changeset} — is exactly what callers already expect from F6.03. An
error pattern passes straight through, broadcasting nothing.
# Portal.Catalog — broadcast a domain event after the write def create_course(attrs) do %Course{} |> Course.changeset(attrs) |> Repo.insert() |> broadcast(:course_created) end defp broadcast({:ok, course} = result, event) do Portal.broadcast("courses", {event, course}) result end defp broadcast({:error, _changeset} = error, _event), do: error
Threading the broadcast through the pipeline keeps the happy path readable and the error path honest. Nothing is broadcast for a failed write, so subscribers never hear about a course that does not exist.
The facade wrapper
The Portal facade wraps subscribe and
broadcast so the server name Portal.PubSub appears once, not
in every context and LiveView. The server itself is started in the application’s supervision tree —
a single child spec — so it is supervised like any other process from F5.
# Portal facade — one home for the PubSub server name and topics def subscribe(topic) do Phoenix.PubSub.subscribe(Portal.PubSub, topic) end def broadcast(topic, message) do Phoenix.PubSub.broadcast(Portal.PubSub, topic, message) end # started once, supervised, in Portal.Application: # children = [{Phoenix.PubSub, name: Portal.PubSub}, ...]
domain emits
The context broadcasts the event after a successful write.
facade hides the server
One wrapper holds Portal.PubSub; topics stay domain concepts.
Next: subscribing a LiveView — the broadcast is out on the topic; now a LiveView joins and reacts to it.