jonnifyelixir
F5 / F5.03 / skeleton

F5.03 · part 2 of 3

The walking skeleton

The walking skeleton is the enroll-a-learner tracer bullet, wired end to end and actually running. A POST /enroll on the thin server calls Learning.enroll/2, which validates the ids, builds an %Enrollment{} with a fresh branded id, and writes it to the store; the handler answers 201. Every layer from F5.01 and F5.02 is present, and every layer is doing the least it can — one route, one context function, one struct, one store call. It is not a demo and not a mock: a real request produces a real, persisted enrollment. That is the skeleton, and everything after this is flesh on these bones.

Follow one enrollment through the round trip. Select a step to see what really happens there.

The round trip

One request goes in and one response comes out, and in between it touches the web layer, the context, the domain struct, and the store — the same vertical slice as the hub, now seen as a sequence. Nothing here is stubbed: the validation runs, the struct is built, the store is written. Select a step.

Enroll, end to end · select a step

POST /enroll request arrives Learning.enroll validate + build Store.put keep it 201 Created answer w/ id every layer present, every layer minimal — and it really runs

step: Learning.enroll/2

does: validate + build

If one real request round-trips, the design is no longer a hope — it is a fact. The skeleton proves the layers fit together before any of them is finished.

In code

Two short pieces, wired. The route from F5.01 maps the engine’s tagged result onto a status code; the context function from F5.02 validates, builds the struct, and stores it in one with chain. Thin at both ends, but every line is real.

# web (F5.01) — one route, maps {:ok}/{:error} to a status
post "/enroll" do
  case Portal.Learning.enroll(conn.params["user"], conn.params["course"]) do
    {:ok, e}         -> send_resp(conn, 201, e.id)
    {:error, reason} -> send_resp(conn, 422, to_string(reason))
  end
end

# context (F5.02) — validate, build the struct, store it
def enroll(user_id, course_id) do
  with :ok        <- validate_ids(user_id, course_id),
       enrollment = %Enrollment{id: Portal.ID.new("ENR"), user_id: user_id, course_id: course_id},
       :ok        <- Portal.Store.put(enrollment) do
    {:ok, enrollment}
  end
end

one path, all layers

Web, context, struct, store — each touched once, none finished.

a skeleton that walks

A real request makes a real, stored enrollment — the frame is proven.

Next: iterating the slice — growing the skeleton one thin vertical slice at a time.

Wire every layer before the logic

Before the validation, the struct, or the store call exist, the enroll path is wired through every architectural layer at once — web entry, the Portal.enroll/2 facade, the Portal.Engine command, and the Portal.EventStore port — with each layer a thin stub that returns a canned value. The single goal is to prove the whole call connects: a POST /enroll reaches the facade, the facade reaches the engine, the engine reaches the port, and a 201 travels back. No id is minted and nothing is persisted; only the wiring is exercised.

This is the tracer-bullet discipline from Hunt and Thomas, The Pragmatic Programmer: the canned layers are not a throwaway prototype but permanent scaffolding. Each stub stays in its place and is fleshed out where it stands — the engine stub gains decide and evolve, the port stub gains a real adapter — so the skeleton that proved the path is the same skeleton the finished system grows from. The shape never moves; only the bodies fill in.

Four layers wired, every body still canned

request ↓ ↑ 201 POST /enroll stub · returns 201 Portal.enroll/2 stub · calls the engine Portal.Engine stub · canned :ok Portal.EventStore stub · no-op append/2 web facade engine port
every layer present · every body canned · the path connects, then each stub is fleshed out in place
# web — the one route, wired to the facade; status is canned for now
post "/enroll" do
  {:ok, _} = Portal.enroll(conn.params["user"], conn.params["course"])
  send_resp(conn, 201, "wired")
end

# facade — reaches the engine; no validation yet, only the wire
def enroll(user_id, course_id) do
  Portal.Engine.command({:enroll, user_id, course_id})
end

# engine — reaches the port and answers; no decide/evolve yet
def command(_cmd) do
  :ok = Portal.EventStore.append("portal", [])   # canned: append nothing to the stream
  {:ok, :enrolled}                       # canned result
end

# port — the seam the real adapter drops into later; a no-op stub today
def append(_stream, _events), do: :ok

# the whole path runs, with not one line of domain logic written
Portal.enroll("USR-1", "CRS-1")
# => {:ok, :enrolled}

the wire comes first

Web, facade, engine, port — all four connected end to end while every body is still a canned stub.

scaffolding, not a prototype

Each stub stays put and is fleshed out in place; the proven frame is the one the system grows from.

Wire the whole path through every layer before writing the logic: once the canned end-to-end call returns, each stub is filled in where it stands, so the skeleton that proved the path is the one the system keeps.

References

Primary sources for this lesson, and where it connects in the course.

Sources

Related in this course