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
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
# 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
- Hunt and Thomas — The Pragmatic Programmer — tracer bullets: build end-to-end before exhaustive.
- Elixir — Introduction to Mix — scaffold the thin, running skeleton app.