skip to content

Your team proposes that every effectful function in a service return an action value applied only at the entry point — how would you weigh that?

level: principalimportance: nice to knowfreq 28%

answer

  1. an architectural bet, not a style choice
  2. name the second interpreter first
  3. dependent steps need sequencing
  4. the runner is code you own
  5. scope it to a boundary, not everything

basics

~20 s

Weigh what a second interpreter is worth against a case set and a runner the team maintains forever. Previews, audits and retries argue for it; opaque stack traces, a sequencing construct for dependent steps, and half-adoption argue against a blanket rule.

solid answer

~40 s

The gain is real and specific: one place that decides when effects happen, so previewing, auditing, permission-checking and retrying are written once, and the code that chooses the work becomes testable as data. The costs are equally specific. Every signature in the service changes. The description must grow a sequencing construct the moment one step needs an earlier step's result. Failures surface in the runner, so a stack trace no longer points at the caller that caused them. And a half-finished migration leaves two conventions in one codebase, which is worse than either. My position is usually to scope it: describe the destructive, auditable or replayable operations as values, keep the rest as ordinary calls, and name the runner as owned code with tests of its own.

go deeper

for a junior

Know the shape of the trade: describing effects gives one place to preview, audit and retry them, and it costs an extra layer that everyone on the team has to learn.

for a middle

Be able to name two concrete costs — dependent steps needing a sequencing construct, and failures surfacing in the runner rather than at the caller — rather than only the benefits.

for a senior

Show how you would scope it: which operations genuinely need a second interpreter, how the runner handles a plan that fails partway, and how a diagnosis still reaches the original request.

for a principal

Own the decision. Say what evidence would make the convention worth its permanent cost, where the boundary sits, who maintains the runner, and what you would do if the migration cannot finish.

## What the proposal actually is The proposal is a codebase-wide convention: effectful functions stop performing and start returning descriptions, and one runner at the service entry point applies whatever description a request produced. It is an architectural bet, not a local style choice, because it changes every signature that touches the outside world and introduces a component — the runner — that the team owns and must maintain. ## What it buys, stated concretely - **One decision point for effects.** Anything you would otherwise sprinkle through call sites — a permission check before a destructive step, an audit line, a retry, a rate limit, a kill switch — is written once, in the runner, against a description it can read. - **Preview as a product feature.** For destructive or expensive operations, printing the plan for an operator to approve stops being a special code path and becomes a second interpreter of the same value. - **Tests that compare values.** The interesting logic is "what should happen", and that becomes a pure function producing a plan. A test asserts on the plan rather than wiring fakes for everything the code might touch. - **Replay and record.** A stored plan can be reapplied or inspected after the fact, which is worth real money in migration and provisioning work. ## What it costs, stated just as concretely 1. **Expressiveness.** A flat description handles independent steps well. The moment step three needs the identifier that step one produced, the description needs a sequencing construct rather than a list, and everyone reading the code has to understand it. This is where teams underestimate the proposal. 2. **Debuggability.** When a step fails, the failure surfaces inside the runner. The trail back to the request that built the plan has to be carried deliberately — a step identity, a correlation value, a builder location recorded in the description — or diagnosis gets harder than it was before. 3. **A component you now own.** The runner is production code with failure semantics, partial-application behaviour, retry policy and tests. Nobody counts that cost during the proposal. 4. **A case set that grows.** Every new kind of effect is a new case plus a matching arm in every interpreter. Blanket adoption means describing effects that will never be previewed, audited or replayed, paying the tax with no return. 5. **Half-adoption.** Two conventions in one service — some functions performing, some describing — is the worst of both, because a reader can no longer tell from a signature whether calling something does anything. 6. **A new silent failure.** A plan can be built, transformed, logged and never applied. That path type-checks and does nothing. ## How I would decide | where it pays | where it does not | |---|---| | destructive multi-step operations operators want to preview | a single read at startup | | work that is audited, approved or replayed | effects on a hot path where the plan is allocated and discarded | | flows whose step selection is the hard part | flows where performing and deciding are one trivial line | | anything that must survive a retry with known semantics | code that is about to be deleted or rewritten | My answer to the team is therefore rarely yes or no. It is: - **Name the second interpreter first.** If nobody can name a runner besides the performing one that the team will actually write, the description is ceremony. That single question settles most cases. - **Decide the sequencing story before adopting.** Dependent steps are the first thing a real service needs; choose how they are described up front rather than discovering it halfway through the migration. - **Scope the convention to a boundary**, such as the operations exposed to operators, rather than to the whole service. A rule that applies to a named set of operations can be reviewed; a rule that applies to everything gets quietly abandoned. - **Budget the runner.** Treat it as a service component with an owner, failure semantics and its own tests, not as glue at the entry point. - **Make the trace deliberate.** Put enough identity into each described step that a failure in the runner names the step and the request that asked for it. ## The answer that reads as senior rather than dogmatic An interviewer is checking whether you can hold both sides. The weak answers are "describe everything, it is the pure way" and "this is over-engineering". The strong answer is that the technique buys one thing — the ability to interpret the work in more than one way, from one place — and that it is worth its cost exactly where a team can name the second interpretation and the failure semantics it wants from the first.

  • What single question would you put to the team before agreeing to the convention?
    Which interpreter besides the performing one will you actually write, and who needs it? A named preview surface, an audit requirement or a replay story justifies the case set. If the honest answer is that only the real runner will ever exist, the description is paying a tax with no return.
  • How would you keep diagnosis from getting worse once failures surface in the runner?
    Give each described step an identity and carry the request's correlation value in the description, so the runner's failure names the step, the plan and the caller that produced it. Without that, the stack trail stops at the runner and the connection back to the originating request is guesswork.
  • The migration will not finish this quarter. Does that change your answer?
    Yes. Half-adoption is worse than either end state, because a signature no longer tells a reader whether calling the function does anything. If the work cannot finish, scope the convention to a boundary that can be completed — one module, or the operator-facing operations — and leave the rest untouched and consistent.

saying these in an interview costs you the question

  • Argues every effect should be described, with no cost named
  • Dismisses the technique as pure ceremony with no benefit
  • Ignores that dependent steps need a sequencing construct
  • Treats the runner as glue rather than owned production code
  • Forgets that failures now surface away from the caller
  • Accepts an indefinitely half-migrated codebase