Le secrétaire de Fernand

Stronger Feedback Loops Start With Explicit Code

Stronger Feedback Loops Start With Explicit Code

Why feedback loops become more powerful when code makes its system concepts explicit.

Central idea — A feedback loop can detect that something broke on its own. It can only tell you what broke — name which form of the system has drifted — to the extent that those forms are explicit: states, contracts, boundaries, invariants, responsibilities, runtime signals.

The short version: 2 min
Full article: 16 min

The short version

A feedback loop does not become powerful just because it exists. It becomes powerful when the code gives it something precise to see. A type checker can only verify meaningful exhaustiveness if states and events are explicit. An architectural audit can only detect drift if boundaries, layers, and responsibilities are visible. A production-like test can only explain a failure if the system emits structured runtime signals.

This is the key idea — the verification surface the code exposes. The more implicit the code, the more a loop can only say something broke; the more explicit, the more it can name which form of the system drifted.

This is the bridge between explicit code and architectural enforcement. First, we make the system visible. Then we build feedback loops that verify the visible forms keep holding.

In three ideas

  1. Local feedback becomes stronger when behavior is explicit. Types, discriminated unions, state machines, exhaustive matrices, pure functions, and narrow contracts give fast checks something real to verify.

  2. Structural feedback becomes stronger when architecture is explicit. Boundaries, layers, imports, ownership, validation points, and responsibilities can be audited only if the system exposes them clearly enough.

  3. Product feedback becomes stronger when runtime behavior is explicit. Critical paths are easier to understand when they emit domain events, structured logs, traces, correlation IDs, rejected inputs, and state transitions.

Takeaway

The problem is not to add more checks around an implicit system. The problem is to write the system so that each feedback loop has something precise to observe.


Full article

In the two previous articles, I argued two things.

First, code is a system. It should not be judged only as elegant prose, but as a structure that has to hold up in reality.

Second, in the age of humans and coding agents, the explicit matters more than the implicit, because the implicit is no longer guaranteed to be shared.

The next step is feedback. Not feedback as more tests piled around the code, but feedback as a way to verify that the explicit forms the system exposes — boundaries, contracts, invariants, states, responsibilities, runtime signals — keep holding while the code is produced.

The point this article turns on:

A feedback loop is only as strong — at naming what went wrong — as the explicit structure it can observe.

If the code hides its states behind scattered booleans, the type checker cannot verify the state model. If boundaries exist only as team knowledge, an architecture rule cannot enforce them. If errors are logged without context, a production-like test can fail without explaining what happened.

Feedback is not external to code design. It depends on the surface of verification that the code exposes.

Feedback is visibility, not reassurance

A weak feedback loop reassures.

A strong feedback loop reveals.

It reveals that a transition is missing. That a boundary has been crossed. That external data entered the system without validation. That a component has taken ownership of logic it should not own. That a critical path failed and the runtime signals are insufficient to explain why.

This matters because reliability is not produced by the volume of checks alone.

Feedback coverage is not fungible. You cannot compensate for a missing kind of visibility by adding more of another kind.

A thousand unit tests will not reveal an architectural boundary that is not represented anywhere. A perfect import audit will not tell you whether a payment flow still works under latency. An end-to-end test will not tell you, by itself, whether a state model is exhaustive.

Each loop has a job. Each loop sees a different kind of deviation. And each loop depends on the system exposing the right structures.

So the question is not:

How many checks do we have?

The better question is:

What does each loop make visible, and what does the code give it to observe?

Detection is cheap; attribution needs structure

It is worth being honest about the limit of this claim, because there is a real objection to it. Some feedback needs nothing from you. A fuzzer throws malformed input at a boundary and finds a crash without knowing a single one of your types. A property-based test asserts an invariant across thousands of generated cases. A black-box integration test hits a real endpoint and notices it returns the wrong thing. None of these depends on the code exposing an explicit internal shape, and all of them find genuine bugs.

So explicitness is not what lets feedback detect that something is wrong. Detection can be opaque.

What explicitness buys is attribution: turning "a test failed" into "this transition is unreachable," "this boundary was crossed," "external data entered unvalidated," "this invariant broke." A fuzzer tells you the system fell over. An explicit state model tells you which move was illegal. The first is a symptom; the second is a cause you can act on directly.

That is the honest scope of the thesis. Explicit structure does not mainly raise how many problems you catch. It raises how precisely each loop can name the problem it catches — and naming the cause instead of the symptom is what lets a human or an agent fix the right thing rather than patch around it. Black-box feedback is the smoke alarm. Explicit structure is what tells you which room.

Explicit code increases the verification surface

The key idea is verification surface. The verification surface is the set of forms in the code that a tool or a reviewer can actually inspect and check against — the handles feedback can grab. An implicit system offers almost none; an explicit one offers many.

An implicit system gives feedback loops very few handles. Many rules live in the developers’ heads. Many states are inferred from combinations of flags. Many boundaries are social conventions. Many invariants are scattered across conditionals. Many responsibilities are implied by file names or habits. Many runtime failures produce only generic errors.

The system may work, but there are few explicit forms for tools to inspect.

An explicit system is different. It gives tools and humans concrete surfaces to verify:

  • types;
  • schemas;
  • discriminated unions;
  • state machines;
  • transition tables and disposition matrices;
  • module boundaries;
  • import rules;
  • public contracts;
  • constructors;
  • domain events;
  • structured logs;
  • traces;
  • correlation IDs;
  • named invariants.

These are not decorative forms. They are handles for feedback.

A type checker can verify types only if types encode something meaningful. An audit rule can enforce boundaries only if boundaries are represented in the codebase. A runtime trace can explain a failure only if the system emits useful events at the right places.

Explicitness does not only make code easier to read. It makes code easier to check.

But surface is not automatically an asset, and I will come back to this. Surface bound to a real system concept pays off every time the code changes; surface bound to an incidental internal detail just gives your checks something brittle to grip.

That is the claim in general. Now look at what it changes, loop by loop.

First loop: local feedback

Local feedback answers the closest question:

Does what I just changed still hold immediately?

This includes formatting, linting, typechecking, compilation, and nearby unit tests. But the interesting part is not that these checks are fast. The interesting part is that they become much stronger when the local code is explicit.

Take a state machine.

If the state of a feature is represented by several booleans, the type checker sees almost nothing:

TypeScript

type RecorderFlags = {
  isRecording: boolean;
  isSaving: boolean;
};

This representation allows states that should not exist:

  • isRecording: true and isSaving: true at the same time;
  • both false, which could mean idle or finished — the flags cannot say which.

The state model exists, but it is implicit. It lives in the developer's interpretation of the flags, and the feedback loop cannot check much.

Now make the state explicit:

TypeScript

type RecorderState =
  | { type: "idle" }
  | { type: "recording"; sessionId: string }
  | { type: "saving"; sessionId: string };

type RecorderEvent =
  | { type: "start-recording" }
  | { type: "segment-ready"; blob: Blob }
  | { type: "save" };

The type checker now sees real structure. Some invalid states are no longer representable. The feedback loop is stronger because the code has given it a better surface. This is the principle Yaron Minsky called making illegal states unrepresentable — pushed one step further: a state the type system cannot represent is a state your feedback never has to chase, because the check is the structure.

You can go one step further. A discriminated union already lets the type checker verify that you handle the states you expect — a switch with a never fallthrough stops compiling the day a new state goes unhandled. The stronger move is to stop treating the events you don't act on as an afterthought, and give every (state, event) pair an explicit disposition. That is a disposition matrix:

TypeScript

type Disposition = "Handled" | "Ignored" | "Stale" | "Rejected" | "Unexpected";

// Total, not Partial: every event must be classified in every state.
type DispositionMatrix<S extends string, E extends string> = {
  readonly [state in S]: { readonly [event in E]: Disposition };
};

const recorderDisposition = {
  idle:      { "start-recording": "Handled",  "segment-ready": "Unexpected", save: "Rejected" },
  recording: { "start-recording": "Ignored",  "segment-ready": "Handled",    save: "Handled"  },
  saving:    { "start-recording": "Rejected", "segment-ready": "Stale",      save: "Ignored"  },
} as const satisfies DispositionMatrix<RecorderState["type"], RecorderEvent["type"]>;

Two things make this stronger than a transition table. First, it is total: the mapped type has no Partial, so every event must be classified in every state. Drop a cell and it stops compiling — "Property 'save' is missing" — so there is no such thing as an unconsidered signal. A blank cell can no longer quietly mean "invalid here"; you have to say which kind of invalid. (A bad disposition, or an event that is not real, is rejected the same way.)

Second, the cell does not record where you go — it records what the signal means here. Look at segment-ready: Handled while recording, Stale while saving (a late event from a phase that has moved on), and Unexpected while idle (a signal that should be impossible — a protocol violation, not a no-op). That is the attribution an earlier section described, precomputed by the type checker. When something goes wrong, the disposition already names the kind: Unexpected pages someone, Rejected is a client error, Stale is benign, Ignored is a deliberate no-op. A transition table can say none of this; it knows only the moves it allows and is silent about everything else.

Three states and three events fit in nine cells you can read at a glance. Real protocols have far more — dozens of events across many states — and that scale, where the matrix becomes a genuine audit surface, is the subject of the next article. The rule does not change with size: every cell, filled and classified.

One honest caveat, because it is the kind of thing this series should not paper over: the matrix is an audit view, not the reducer itself. The types prove every cell is filled; they do not prove the reducer actually behaves as each cell claims. That correspondence is a second guarantee — completeness checked by the compiler, agreement checked by a test. Two loops, which is rather the point.

This is the bridge from the previous article: explicit code gives local feedback more to work with.

Second loop: structural feedback

Structural feedback asks a different question:

Does the code still have the shape the system is supposed to keep?

This is where architecture rules, import constraints, dependency checks, layer conventions, and architectural fitness functions belong.

But again, these loops only work if the system exposes its structure — and the pattern is the same each time: make the concept explicit and a rule can check it; leave it implicit and there is nothing to inspect.

If boundaries are explicit — domain/ must not import infrastructure/, ui/ must not call database adapters directly, public modules expose contracts rather than internals — an import rule can detect boundary drift. If contracts are explicit — payloads parsed at the boundary, handlers validating input before the domain, DTOs kept distinct from internal models — feedback can ask a real system question: can unvalidated data enter the core? That is not a generic lint rule; it exists because the code made its validation points visible. And if responsibilities are explicit — components render state but do not own business transitions, domain services enforce domain rules, adapters perform effects behind ports — feedback can catch responsibility drift: business logic creeping into components, infrastructure imported into the domain, state mutated from several authorities, side effects in functions meant to be deterministic.

This is what separates structural feedback from local feedback. Local feedback says this code runs. Structural feedback says this code still belongs where it was placed.

Third loop: product / production-like feedback

Product feedback asks a third question:

Do the critical paths still hold when the system meets something close to reality?

This includes integration tests, targeted end-to-end tests, smoke tests, staging checks, and deliberately degraded scenarios: latency, unavailable dependencies, rejected inputs, partial failures.

Like the others, this loop is only as readable as the signals the system emits. A production-like test that fails without runtime signal gives you a symptom — something broke — but not what the system was doing, which state it reached, which input it rejected, or which dependency failed. The stronger version attaches observability to system concepts: a domain event when a recording starts, a logged transition from recording to saving, rejected inputs carrying structured reasons, external calls carrying correlation IDs, critical handlers emitting business context rather than only technical errors. This is where SRE practice and standards like OpenTelemetry matter — they make runtime behavior readable, and most readable when the instrumentation is tied to explicit concepts.

The goal is not to know that a path failed, but to read its trajectory:

What entered the system?
Which boundary accepted or rejected it?
Which state transition occurred?
Which dependency was called?
Which invariant held or broke?
Which user-visible effect followed?

A product loop is strong exactly when it can answer those — which it can do only if the system emits the signals to answer them.

The three loops are not interchangeable

Once feedback is understood as visibility, the loops are clearly non-fungible.

More local feedback cannot replace missing structural feedback. It may prove that individual units behave correctly, but it cannot prove that boundaries are respected if boundaries are not being checked.

More structural feedback cannot replace product feedback. It may prove that the system keeps its intended form, but it cannot prove that critical paths hold under real latency, real dependency behavior, or real user sequences.

More product feedback cannot replace local or structural feedback. It may reveal a failure, but without local and structural surfaces it often finds the symptom later, farther away from the cause.

Misplaced feedback raises correction cost because the symptom and the cause appear in different loops.

A product test fails, but the cause is an implicit state model. A review detects architecture drift, but the cause is an unrepresented boundary. A runtime error appears, but the cause is a missing validation contract.

The later the mismatch is discovered, the more expensive the correction becomes.

The solution is not more checks everywhere. It is better visibility at each level.

LoopWhat it revealsWhat more of another loop cannot replaceWhat explicit code gives it
Localimmediate errors, exhaustiveness gaps, invalid statesmore E2E tests cannot make implicit states exhaustivetypes, unions, state machines, pure functions
Structuralboundary drift, responsibility drift, dependency driftmore unit tests cannot prove architectural shapemodules, layers, contracts, import rules, ownership
Productruntime failures, degraded paths, missing observabilitymore audits cannot prove real behavior under real conditionsevents, traces, logs, correlation IDs, state transitions
Human reviewmodel quality, abstraction quality, trade-offsmore automation cannot decide whether the model is rightreadable design, explicit intent, clear responsibilities

This table is the core of the approach.

Not because it lists checks, but because it links three things:

  1. the kind of deviation we want to see;
  2. the loop that can see it;
  3. the explicit structure that makes it visible.

Explicit, but not maximal

A warning, because the argument is easy to overshoot. More explicit forms do not automatically mean stronger feedback. The previous article already made the parallel point — more code does not automatically mean more robustness — and it holds here too.

Explicit structure helps feedback only when the structure is a genuine system concept: a boundary, a state model, an invariant, a contract, a responsibility. Make those explicit and your checks grip something that is supposed to stay stable. Bind a check to an incidental internal detail instead — the exact shape of a representation you actually want to keep changing — and you have not bought visibility, you have bought brittleness. The check now breaks on refactors that changed nothing that mattered, and people learn to switch it off.

The disposition matrix above shows both sides. It is powerful when the disposition of the protocol is a real domain concept the system must preserve — and forcing every cell is exactly what stops a signal from being silently forgotten. It becomes a liability the moment it hard-binds your feedback to an internal arrangement you want to keep refactoring, or balloons into cells nobody can keep honest. The discipline from the second article applies unchanged: make explicit — and check — the forms that carry the system, not the ones that merely happen to exist. Verification surface is worth creating only where it tracks something the system must keep true.

Automated loops do not replace human review

These loops do not replace human judgment.

They protect it.

A human review should not spend most of its energy on formatting, forbidden imports, obvious typing errors, missing exhaustiveness, or structural violations that a machine can detect.

The machine should report what it can report.

The human should focus on what the machine cannot decide alone:

  • Is the model right?
  • Is the boundary in the right place?
  • Does this abstraction help?
  • Is this trade-off acceptable?
  • Does the behavior match the real need?
  • Does the system remain understandable?

Automation is strongest when the code exposes explicit forms.

Human judgment is strongest when automation has already cleared away what does not require judgment.

In the age of agents, explicit feedback is coordination

This becomes even more important with coding agents.

An agent can produce code quickly. It can satisfy a local instruction. It can pass tests. It can also misread a convention, bypass a boundary, duplicate logic, introduce implicit state, or place a responsibility in the wrong layer.

The agent does not need vague feedback. It needs precise feedback.

Not just:

This is wrong.

But:

This event is not handled in this state.
This module crosses a forbidden boundary.
This handler accepts unvalidated input.
This component owns business logic.
This critical path fails without enough runtime context.

Explicit feedback gives the agent a better correction target.

This is why explicit code and feedback loops reinforce each other.

Explicit code gives the loops more to observe. Stronger loops give humans and agents sharper signals. Sharper signals make it easier to preserve the system while changing it.

Feedback becomes a coordination layer between:

  • the human who defines the system;
  • the agent that produces changes;
  • the codebase that encodes the rules;
  • the runtime that reports what happened.

Without explicit structure, the agent is guided mostly by tests, errors, and textual instructions.

With explicit structure, it is also guided by types, boundaries, contracts, transitions, invariants, and runtime signals.

That is a different level of control.

Conclusion

The point of layered feedback loops is not to add more checks.

It is to make the system more visible at each level where it can drift.

Local feedback reveals whether the immediate change still holds. Structural feedback reveals whether the code still has the shape of the system. Product feedback reveals whether critical paths hold when they meet conditions close to reality. Human review judges whether the model, abstraction, and trade-offs are right.

But none of these loops is strong by itself.

They become strong when the code gives them explicit structures to observe.

That is why feedback loops belong in this series. The first article argued that code is a system. The second argued that system concepts must be made explicit. This one adds the next step: once the system is explicit, we can build feedback loops that verify those explicit forms keep holding.

A feedback loop is not just a check around the code. It is a way of reading what the code has made visible.

Detection tells you the system moved. Attribution tells you which form moved. And only feedback that can attribute lets a system change without losing its shape.

Further reading

These works came before this article and go deeper than it does. I arrived at many of the same ideas through practice rather than from them — and put names to several of them only while writing this piece. So I'd rather point you to the people who developed them properly than pretend the thinking is mine. Where they and this article disagree, they have earned more trust than I have.

  • Yaron Minsky — Effective ML / “make illegal states unrepresentable”: names the same idea the local-feedback section arrives at.
  • John Hughes — QuickCheck and the property-based testing tradition: feedback that detects without any explicit internal structure (the counterexample this article has to answer).
  • Dan North — on testing behaviour rather than implementation: the case for not coupling checks to internal form.
  • Neal Ford, Rebecca Parsons, Patrick Kua — Building Evolutionary Architectures: the source of the term fitness functions used in the structural loop.
  • Charity Majors, Liz Fong-Jones, George Miranda — on modern observability: the runtime end of the product loop.

A thought after reading?

If you would like to discuss about this article, you can write to me here. I share because I care and I want to learn. Please teach me with care.