Months on, your renewal shell is full of conditionals while the pure core barely decides anything — what happened?
answer
- the branches migrated outward
- a rule needed one more read
- dispatch only, outside
- a fetch function is not a value
- load wider or decide in rounds
basics
~20 sDecisions leaked outward, usually because a rule needed one more record and someone wrapped a condition around the fetch instead of feeding the data in. Every such branch is logic living in the half that only slow tests reach.
solid answer
~40 sThe arrangement erodes one convenience at a time. A rule needs a record nobody loaded, so a condition appears around the fetch; a rule needs the outcome of a charge, so another condition appears after it. Each is a business rule now sitting in the layer that talks to the world. The diagnosis is a single rule: the only branching the outer layer may contain is dispatch on which kind of decision came back — anything that reads an account's own fields is a decision that escaped. Repairs are to widen the up-front load, or to decide in rounds where the core says what more it needs. What is not a repair is handing the core a function it can call to fetch, which restores the impurity while looking like parameterisation.
go deeper
Learn the symptom to look for: conditions about accounts sitting in the code that talks to the outside. That is deciding happening in the wrong place, whatever the file is named.
Explain the review rule — the outer layer branches on which kind of decision came back and nothing else — and be able to say why a retry is a legitimate exception to it.
Show the repair path and price it: widen the gather, or decide in rounds. Say out loud why handing the core a fetch function is not a repair even though it tests cleanly.
Set the boundary as a standard other teams can apply, with a measurement attached, and be honest about where the overhead is not worth it — a thin pass-through with one rule rarely repays the extra round trip.
## How the outer layer fills up Nobody decides to abandon the split. It erodes through individually reasonable steps: 1. A new rule needs a record that was not gathered. Loading it inside the decision is obviously wrong, so the fetch goes outside — and the condition that says *whether* to fetch goes with it. 2. A rule depends on whether the previous call succeeded, so a condition appears after the call, where the outcome is. 3. A special case is urgent, the core's shape does not fit it, and the branch lands outside "for now". 4. Someone reuses an existing test fixture rather than adding a case over values, and the code follows the test. After a few months the deciding happens outside and the core does arithmetic. Everything still works; what has been lost is that the rules now live where each new case costs a real boundary to exercise. ## The one branch the outer layer is allowed A rule sharp enough to use in review: **the outer layer may branch on which kind of decision came back, and on nothing else.** A condition that reads an account's own fields — its status, its tier, its dates, its balance — is a rule, and rules belong in the core. Two things look like violations and are not: - **Retry or fallback around a failed call.** That is about the call's outcome, not about the account. It is outer-layer material by right. - **A guard on whether the outside system is reachable at all.** Also about the world, not about the rule being applied. The test to apply is: could this condition be evaluated from data alone, with nothing running? If yes, it belongs inside. ## Three repairs, and what each costs | repair | what it does | what it costs | |---|---|---| | widen the gather | the outer layer loads the superset of records the rules could need | fetching data that some runs will not use | | decide in rounds | the core returns what further records it needs; the outer layer fetches and calls it again | an extra round trip, and a core that can say "not yet" | | move the branch back inside | the condition becomes a rule over a field already in hand | sometimes a wider input record than before | All three keep every read strictly between calls to the core rather than inside one. Which to pick is a cost question: a small bounded superset is usually the simpler bet, while a large or expensive fan-out favours rounds. ## The repair that looks right and is not The attractive fix is to pass the core a function it can call when it needs another record. It parameterises the dependency, it can be substituted in a test, and it keeps the code reading top to bottom. It also puts the read back inside the core. Hold the distinction firmly: **a value is fixed at the boundary; a function is a door the core can walk through whenever it likes.** Once the core can open that door, its result depends on when it was called and on what came back, so the same arguments can produce two answers. You have made the dependency visible and controllable, which is a real improvement over reaching for a global — but the core is no longer pure, and the properties you built the split for (replaying a run from its arguments, cases as literal values, any ordering) go with it. ## Watching it over time Erosion is gradual, so watch numbers rather than intentions: - The count of conditions in the outer layer that read an account's fields. It should stay near zero and is trivial to check in review. - The share of cases that need a boundary to run, and the suite's wall-clock time. Both climb when rules land outside. - How often a new rule arrives together with a new slow case — the clearest single signal. - The size of the input record the core takes. It growing is usually **healthy**: it means data is being gathered up front instead of fetched mid-decision. ## What to say in the room Name the mechanism rather than the pattern. The split did not fail; the rules left the part that was built to hold them, each time in exchange for a small convenience. The way back is the same, in reverse: take one leaked condition, identify the record it needed, load that record up front or add a round, and move the condition inside. Do it for the branches with the most rules behind them and the curve turns without a rewrite.
- Why is passing in a function that performs the read not the same as passing in a value?A value is fixed at the boundary; a function is a door to the outside that the core can walk through whenever it chooses. The result then depends on when it was called and what came back, so identical arguments can give different answers. It becomes controllable in a test, but it has stopped being pure.
- How do you choose between over-fetching and deciding in rounds?Weigh the cost of data you load but may not use against the cost of another round trip plus a core that can report what it still needs. A small, bounded superset is usually the simpler bet; a large or expensive fan-out, or one whose size depends on the first round's answer, favours rounds.
- What measurement tells you the split is eroding?Count the conditions in the outer layer that read an account's own fields, and watch how many new rules arrive with a slow test attached. Suite wall-clock time is the lagging version of the same signal. A growing input record to the core, by contrast, is usually a sign the arrangement is healthy.
saying these in an interview costs you the question
- Treats an injected fetch function as equivalent to a passed value
- Calls the outer layer thin while it holds most branches
- Says an extra round trip is always too expensive to consider
- Accepts one read inside the core as a harmless exception
- Blames the arrangement rather than the rules that leaked out