jonnifyelixir
F5 · Pragmatic Programming / F5.08 · boundaries

F5 · the engine · module 8

Boundaries & integration seams

Seven modules in, the engine works: a pure core of decide and evolve, a supervised GenServer that holds the fold, and a test suite that pins it down. What it does not yet have is a boundary. Right now a caller would have to know the engine is a process, know the message shapes it accepts, and know how state is stored. That is three kinds of coupling the web layer in F6 should never carry. This module draws the boundary the way Alistair Cockburn frames ports and adapters — the application core defines its edges as interfaces and the outside world plugs into them — rendered in Elixir as behaviours. The core states what it needs (a driven port such as an event store) and how it is invoked (a driving port, the facade the UI calls); adapters supply the implementations; and every failure that crosses the line does so as a typed error contract the UI can render.

One core, two kinds of port, one error vocabulary. Select a seam to see what it is and which dive builds it.

What the caller must know

THE CALLER the UI · F6 LiveView knows it’s a process knows the message shapes knows the store ENGINE CORE decide · evolve · state

[ process · messages · store ]
Three couplings the caller must carry.

The shape of the boundary

The hexagon is Cockburn's picture: the application core sits inside, ignorant of what is outside it, and each edge is a port — a named interface. Things that drive the application (a web request, a test, a CLI) come in through a driving port; things the application drives (a database, a clock, a message bus) sit behind driven ports. The crucial rule is the direction of dependency: the core depends on its own ports, never on the adapters that implement them. Select a seam to see where it sits and what it carries.

The hexagon · select a seam

ENGINE CORE decide · evolve · state adapters: InMemory · Postgres DRIVEN PORTS EventStore · Clock caller: LiveView (F6) DRIVING PORT the Portal facade ERROR CONTRACT %Portal.Error{} → UI

seam: Driven ports

in the Portal: behaviours the core needs

A boundary is not a folder layout; it is a set of named interfaces with the dependency arrow pointing inward. Get the arrow right and the database, the web, and the clock all become details.

Three seams in code

Each seam is one small Elixir construct. A driven port is a @behaviour the core calls and config resolves to an adapter; the driving port is a context module of intention-revealing functions; the error contract is a struct with a closed set of codes. Seen together before the dives unpack them:

# 1. a driven port — the core depends on this behaviour, not on any adapter
defmodule Portal.EventStore do
  @callback append(stream :: String.t(), [struct]) :: :ok | {:error, term}
  @callback read_stream(stream :: String.t()) :: {:ok, [struct]} | {:error, term}
end

# 2. the driving port — the only surface the UI in F6 will call
defmodule Portal do
  def enroll(user_id, course_id), do: Engine.command({:enroll, user_id, course_id})
end

# 3. the error contract — a closed vocabulary the UI can render
defmodule Portal.Error do
  @type code :: :already_enrolled | :course_not_found | :invalid_progress
  defstruct [:code, :message, :field]
end

The config line that picks an adapter — config :portal, :event_store, Portal.EventStore.Postgres — is the whole of the wiring; tests set it to Portal.EventStore.InMemory and nothing in the core changes.

Three deep dives

In order: ports as behaviours and adapters as their implementations, with the dependency arrow pointing inward; the facade as the one stable door the UI calls; and the error contract that turns internal failures into a finite set of outcomes the UI can render.

F5.08.1
Ports & adapters
A port is a @behaviour; adapters implement it; config swaps them. One event-store contract, an in-memory adapter and a Postgres adapter, and a core that never names either.
F5.08.2
The engine facade
A small, intention-revealing context — enroll/2, deliver_lesson/2, progress_of/1 — that hides the GenServer, the log, and the reducer behind one door.
F5.08.3
Error contracts for the UI
A closed set of %Portal.Error{} codes, mapped at the boundary, so the UI renders a finite set of outcomes and never sees a raw exception or an internal struct.

F5.07 · a tested engine

The core is pure and pinned by tests, but its callers still have to know it is a process with message shapes.

F5.08 · a usable boundary

Ports invert the dependencies, a facade hides the runtime, and errors cross the line in a shape the UI knows.

Start with ports & adapters, then the engine facade, then error contracts for the UI. The next module, F5.09 — the engine lab, assembles the facade end to end and mounts it behind a LiveView sketch for F6. The engine behind this boundary is F5.06 · where engine state lives, and the runtime path is the design brief the command & event flow.

References

Ports and adapters, behaviours as the port mechanism, and adapters chosen by configuration.

Sources

Related in this course