jonnifyelixir
F6 / F6.07 / broadcast

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

broadcast/3 broadcast(server, topic, msg) — deliver to every subscriber the call a topic "courses" — a string name that messages travel on the channel the PubSub server Portal.PubSub — the named registry, started in the tree the router

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

ANY CALLER web form live form import script CONTEXT · create_course/1 Repo.insert() → {:ok, course} broadcast {:course_created, course} PubSub "courses" on to every subscriber the event is emitted inside the domain — the web layer only subscribes, never broadcasts

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.