Chapter 7 composed parts horizontally: specifications combined with other specifications, all at the same level, all the same type. This chapter composes them vertically. Layering takes an existing behavior and wraps it in a new stratum that adds something — a check, a measurement, a cache, a retry — while delegating the real work downward. The base does not know it has been wrapped; the wrapper adds its concern and passes the rest along.
The two techniques are complements. Combinators build a wide vocabulary out of peers. Layering builds a tall stack out of strata, each responsible for one thing, each defined against the same interface so the whole tower is still, from the outside, a single value of that type. Together they are the closed, typed half of composition — everything the type system can check — and the last thing to master before Part IV steps outside it.
8.1 One Interface, Many Strata
Return to the order workflow. The pure decision (Chapter 5) and its effects (Chapter 6) are the base behavior; call the thing that runs an order placement a Handler. Around that base we will want cross-cutting concerns — logging, metrics, idempotency — that have nothing to do with the domain and everything to do with operating it. The layering move is to give every one of them, base and concern alike, the same interface:
-- Every stratum, from the core to the outermost concern, is a Handler.
deferred class Handler
feature
handle(d: Draft): Result[Placement, String] do end -- each layer overrides
end
-- The base: the domain work itself, wrapping nothing.
class Core_Handler inherit Handler
create make() do end
feature
handle(d: Draft): Result[Placement, String]
do
result := place(d) -- the pure transition from Chapter 5
end
end
Because a layer is a Handler and also holds a Handler, layers stack. This is the closure property of Chapter 7 again, but pointed vertically: the output of wrapping is the same type as its input, so a wrapped handler can itself be wrapped.
8.2 A Stratum That Adds One Concern
A layer holds the handler beneath it, does its own small job before or after, and delegates the domain work down. Here is a logging stratum: it announces the attempt, calls inward, and reports the outcome — and touches no domain logic at all.
class Logging_Layer inherit Handler
create make(next: Handler, log: Logger) do inner := next logger := log end
feature
inner: Handler
logger: Logger
handle(d: Draft): Result[Placement, String]
do
logger.info("placing order with " + d.items.length.to_string + " item(s)")
result := inner.handle(d) -- delegate downward
match result of
when Ok as ok then
logger.info("placed, total " + ok.order.total.to_string)
when Err as e then
logger.warn("rejected: " + e.error)
end
end
end
Two things are worth noticing. The layer does its work around the delegation — before the call and after it — which is exactly the power a wrapper has that a side-by-side combinator does not: it surrounds the base rather than sitting next to it. And the concern is completely isolated: everything about logging lives in this one class, and nothing about logging appears anywhere in Core_Handler or in the domain. A metrics layer, a retry layer, an idempotency layer each look the same — one class, one concern, delegating the rest.
8.3 Stacking
Assembling the system is now a matter of nesting the strata, innermost last:
-- Read outside-in: log around metrics around the core.
let pipeline: Handler :=
create Logging_Layer.make(
create Metrics_Layer.make(
create Core_Handler.make(),
metrics),
logger)
let outcome: Result[Placement, String] := pipeline.handle(draft)
From the caller's side, pipeline is just a Handler — the same type as the bare Core_Handler. The caller cannot tell, and need not care, whether it holds one layer or ten; it calls handle and gets a Result[Placement, String]. That is the sense in which layering stays closed: however tall the stack, the outside sees a single value of the interface type, fully checked by the compiler.
This is stratified design in the sense Sussman and Hanson describe it: each stratum is a complete version of the behavior. Peel off the logging layer and the metrics-wrapped core is still a working Handler. Peel off metrics too and the bare core still works. A layer refines what is beneath it without being necessary to it, so you can run any suffix of the stack and get a coherent system — which is precisely what makes the strata independently testable: wrap a stub Handler to test a layer in isolation, or run the core alone to test the domain without any of the operational dressing.
8.4 Additive — and This Time, Ordered
Layering is additive in the same way combinators are: a new concern is a new class, inserted into the stack, with no edit to the base or to any sibling layer. Want request tracing? Write a Tracing_Layer and slot it in. The core never learns of it; the other layers never learn of it; the type that flows through the stack never changes. This is the open-by-extension property the whole book is building toward, achieved here without leaving the type system — every layer is a checked Handler.
But layering introduces a subtlety combinators mostly avoid: order matters. The Both of Chapter 7 is commutative — "domestic and over $100" means the same as "over $100 and domestic". A stack of layers usually is not. Put retry outside logging and you log once per attempt; put it inside and you log once per outcome. Put idempotency outside the charge and a retried request is charged once; put it inside and it may be charged twice. The strata are independent in what they do but coupled in the order they do it, and that order is a genuine design decision, not a detail. It is the price of the extra power: a wrapper that can act before and after the base is a wrapper whose position in the stack has consequences.
8.5 A Layer Must Keep the Base's Promise
There is one obligation a layer carries that the type system states only in part. Because a layer is substitutable for the handler it wraps — both are Handler — any caller relying on the base's behavior must not be surprised by the wrapper. A layer may add to what happens; it must not break what the base promised. A logging layer that swallowed an Err and returned a fabricated Ok would type-check perfectly and corrupt the system.
This is the substitutability principle, and it is a contract, not a type. The interface guarantees the shape — a layer returns a Result[Placement, String] — but "the layer preserves the outcome the base computed, adding only its own concern" is a behavioral promise the compiler cannot see. State it where such promises belong: as a postcondition on handle that every layer must honor, so that wrapping a well-behaved handler yields a well-behaved handler. A well-formed stratum is one whose contract implies the contract of what it wraps. Keep that, and a tower of layers is as trustworthy as its base; break it in any single layer, and the tower silently lies.
8.6 The End of the Bridge
With combinators and layering, Part III has assembled a complete toolkit for building behavior without editing existing behavior — horizontally, by composing peers into a wide algebra, and vertically, by stacking strata into a tall one. Both are additive; both are open to extension; and, decisively, both stay inside the type system. Nothing in Parts I through III has asked the compiler to fall silent. We have been flexible, but only in the ways a type checker can still vouch for.
That is about to change, and on purpose. Everything so far shares a hidden limit: to add a new peer or a new stratum, the new thing must fit an interface fixed in advance. A Spec[T] must implement holds; a Handler must implement handle. But some kinds of growth cannot be predicted well enough to name an interface up front — where you want to extend an operation to a new type of data that the operation's author never anticipated, or grow a language's own vocabulary at runtime. There the interface itself must be left open, the dispatch must happen on shapes the compiler does not know, and the type system can no longer carry the argument.
That is the frontier, and it is where contracts stop being a supporting player and become the guard. Part IV steps across the line — beginning with generic operations, the first deliberate move outside the type system, and the chapter where the membrane finally does its real work.