jonnifyelixir
F5 / F5.04 / fail-fast

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

FAIL FAST check at the boundary, before any change cheap · close to cause FAIL LATE surfaces deep, after the damage costly · far from cause

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

Related in this course