F5.04 · part 3 of 3
Failing fast
A contract is only useful if it is checked at the right moment: at the boundary, before the command does anything. Check the precondition first, and a violation stops the command before a struct is built or the store is touched — the error lands right next to its cause and nothing downstream is corrupted. Skip the check, or bury it deep, and bad input flows inward to surface much later as a broken enrollment or a crash far from where it started. Failing fast also means splitting the two kinds of failure: an expected one returns a tagged error the caller handles; an impossible one — a broken invariant — crashes, because there is no sensible way to continue.
Two places a contract can break. Select one to see where the failure surfaces and what it costs.
Early or late
The same violation costs almost nothing caught early and a great deal caught late. Early, the inputs are still in hand and the cause is obvious; late, the bad value has been copied, stored, and passed on, and the crash names a victim rather than a culprit. The discipline is one rule: check, then act — never act, then discover. Select where the failure happens.
Where it breaks · select one
approach: Fail fast
surfaces: at the boundary, before any change
Check, then act. A contract enforced after the work is done is not a contract; it is a post-mortem.
In code
The gate goes first. The with chain checks the precondition before
build or put ever runs, so a bad request returns
{:error, _} with the store untouched. The commented-out version does the opposite
— it builds and stores, then checks — and by then the damage is already on disk.
# fail fast — check first, then act: nothing is built or stored until the contract holds def enroll(user_id, course_id) do with :ok <- check_precondition(user_id, course_id) do # the gate, up front enrollment = build(user_id, course_id) :ok = Portal.Store.put(enrollment) {:ok, enrollment} end end # fail late (avoid) — act first, discover the problem after the damage: # enrollment = build(user_id, course_id) # Portal.Store.put(enrollment) # a bad enrollment is now persisted # check_precondition(user_id, course_id) # too late to matter
check, then act
The precondition is the first thing the command does, not the last.
close to the cause
A boundary failure is cheap to find; a buried one corrupts state and hides.
That closes F5.04: every command has a contract, written in Elixir idioms and enforced at the boundary. The next module, F5.05 — Commands, queries & events, separates writes from reads and turns each change into a domain event. Back to the module overview or the chapter.
References
Primary sources for this lesson, and where it connects in the course.
Sources
- Bertrand Meyer — Applying Design by Contract — preconditions, postconditions, invariants.
- Eiffel — Design by Contract and assertions — the contract metaphor, in depth.
- Elixir — Patterns and guards — guards as executable preconditions.