skip to content

A marker on a payroll method quietly makes the call transactional and retried, so what does that cost the reader of the call site?

level: middleimportance: must knowfreq 62%

answer

  1. the call site stops explaining itself
  2. cause and effect sit far apart
  3. action-at-a-distance
  4. frames nobody wrote in the timeline
  5. one deleted line changes behaviour

basics

~10 s

Action-at-a-distance: the call site stops describing what the call does. Behaviour now depends on a marker plus machinery the reader cannot see, so local reasoning, debugging, review and onboarding all get more expensive.

solid answer

~40 s

The call `runPayroll(batch)` no longer predicts its own behaviour. A marker on the declaration, plus something that acts on that marker, decides that the call runs inside a transactional boundary and is re-attempted after a failure — and none of that is written where the call happens. That is action-at-a-distance: the cause sits far from the effect. Four costs follow. **Local reasoning** breaks, because reading the path is no longer enough; you must know the marker convention. **Debugging** gets harder, because frames and timing appear that nobody wrote at the call site. **Review** gets harder, because deleting one marker line is a behaviour change that looks like a formatting change. And **absence is silent** unless somebody built something to notice it. The answer is not to abandon markers, but to make the wiring discoverable.

code

pseudocode · 12 lines
pseudocode
<<transactional>>
<<retried max 3>>
function runPayroll(batch)
    for each employee in batch
        postToLedger(employee, netPay(employee))
    end
end

function nightlyJob()
    runPayroll(todaysBatch)
    // nothing on this line mentions a boundary or a second attempt
end

go deeper

for a junior

Recall the shape: metadata attached to a declaration can cause real behaviour, so a call can do more than its line says. When something surprising happens, look at the declaration's markers, not only at the body.

for a middle

Explain the mechanism in both directions: the marker selects the behaviour, separate machinery applies it, and the call site is left with no record of either. Name the four costs — local reasoning, debugging, review, silent absence.

for a senior

Show that you have paid these costs in production. Describe how you made the wiring visible: behaviour that emits its own trace events, an effective-wiring report, tests that fail when a marker stops taking effect.

for a principal

Frame it as a trade you own for the organisation: uniform policy and one place to change it, against a reader population that must learn a convention. Say how large a marker vocabulary you would let a codebase carry, and why.

## The two jobs a marker can have A **marker** is metadata fastened to a declaration — a method, a class, a field, a parameter. It is a name, sometimes carrying members, that travels with the declaration. What the marker *means* is not decided by the marker; it is decided by whatever reads it. So the same syntactic thing can do one of two very different jobs: - **Description.** Something reads it and produces a report, an index entry, a warning, a piece of documentation. What the program does when it runs is the same with the marker and without it. - **Instruction.** Something reads it and changes what happens when the declaration is used: a boundary is opened around the call, the body is re-attempted after a failure, a value is routed elsewhere, a check is inserted before the body runs. The payroll method is the second kind. Two markers on the declaration turn an ordinary call into a call that participates in a transactional boundary and is retried. Nothing at the call site says so. ## Action-at-a-distance, stated precisely **Action-at-a-distance** is the property that the cause of an observed behaviour is not visible from the place where the behaviour is observed. A reader standing at `runPayroll(todaysBatch)` sees a name, an argument and a return. The transaction and the retry are real, but their cause is two hops away: the marker on the declaration, and the machinery that acts on markers of that kind. Neither hop is reachable by reading the path in front of you. This is not the same as ordinary indirection. Calling through an interface also hides the implementation, but the call site at least tells you that a call is being made and what its contract is. Here, the call site is *complete and correct as written* and still fails to predict behaviour. ## The four costs, named 1. **Local reasoning.** The unit of understanding grows from "this file" to "this file plus the convention". A reader who does not know the convention reads the code correctly and still gets the wrong answer about what it does. 2. **Debugging.** During an incident, the timeline contains events nobody wrote: a second attempt at the same work, a boundary opening and rolling back. Frames appear between the caller and the body. A maintainer who does not expect them spends the first minutes deciding whether they are a bug. 3. **Review.** A diff that deletes one marker line looks trivially small. It is a behaviour deletion. Reviewers who read diffs for *lines changed* rather than *behaviour changed* will wave it through. 4. **Silent absence.** A missing instruction marker produces no error by itself. The payroll run simply is not retried, and nothing says so until a failure that would have been absorbed becomes a failed run. Absence is silent unless something was built to notice it. ## What the reader has to do, side by side | The reader's question | Explicit call in the body | Marker on the declaration | |---|---|---| | Does this call retry? | Read the lines; the loop is there | Read the lines, then know which markers cause retries | | Where do I change the policy? | Every call site that has the loop | One place, for every marked declaration | | What does a small diff mean? | Small diff, small behaviour change | A one-line diff can change behaviour everywhere | | Who notices if it is dropped? | The removed code is visible | Nothing, unless a report or a test watches for it | ## Why teams take the trade anyway The trade is usually made on purpose, and the reasons are good. Cross-cutting policy written by hand at hundreds of call sites drifts: three of them get the retry count wrong, one forgets the boundary entirely, and the policy can never be changed centrally again. A marker states the intent once per declaration and lets one implementation apply it uniformly. The declarative form also keeps the intent **next to the code it governs**, which is a real advantage over the same policy expressed far away in a separate configuration artifact — the reader who opens the declaration at least sees that *something* is attached. So the honest framing is not "markers are bad". It is that a marker moves a cost rather than removing it: repetition and drift go down, and the distance between cause and effect goes up. A team that takes the trade owes the reader a way to close that distance. ## Shortening the distance - Make the behaviour **announce itself** at run time — a log line or a trace span emitted by the machinery, so a retry is visible in the timeline rather than inferred. - Publish the **effective wiring**: which declarations actually got which behaviour, produced by the same thing that applies it, not by a document. - **Pin the behaviour with a test** so that its disappearance is a build failure rather than a production surprise. - Keep the **vocabulary small**. Five markers everyone knows cost far less than forty that nobody has read. - Treat a marker change in review as a **behaviour change**, with the same scrutiny as an edit to the body.

  • Why is an incident timeline usually where this cost first becomes concrete?
    Because the timeline shows effects without their causes: a boundary opening, a second attempt at the same work, extra frames between caller and body. A maintainer who does not know the convention must first decide whether those events are the bug or the design, and that decision costs the opening minutes of the incident.
  • Does writing the behaviour explicitly at every call site remove the cost?
    No — it trades one cost for another. Explicit calls make each site self-explaining, but the policy is now copied, so it drifts and can no longer be changed in one place. The choice is which cost you would rather pay, and for behaviour that is genuinely uniform the marker usually wins, provided the wiring is made discoverable.
  • Is a marker that keeps policy next to the code better than the same policy in a separate configuration artifact?
    Usually yes on discoverability: the reader who opens the declaration sees that something is attached, which the distant artifact cannot offer. It is not automatically better on everything else — a marker is edited and shipped with the code, so it cannot be changed without a release, and the distance to the machinery that acts on it is unchanged.

A house rule posted in the hallway rather than in the room. Everyone in the room behaves differently, and nothing inside the room explains why.

saying these in an interview costs you the question

  • Says the marker itself opens the transaction and performs the retry.
  • Claims the call site obviously shows that the call is retried.
  • Treats deleting a marker line as a cosmetic, comment-level change.
  • Thinks markers cost nothing because they remove repetition.
  • Assumes a failure trace will point straight at the responsible marker.
  • Says the cost disappears once the convention is written in a document.