# One write path, many callers

> When the same change can be made from several places — a back-office screen, a field app, an agent tool — each entry point is a chance to drift. Route them all through one core function that owns the write and emits the canonical event. A caller that writes the row directly will look correct and silently skip every downstream effect.

URL: https://biloh.com.au/docs/engineering-notes/one-write-path-many-callers
Category: Engineering notes | Audience: builder | Updated: 2026-06-26

A job marked complete from the office drafted an invoice. The *same* job marked complete from the field app did not — even though both set the status to `completed` on the same row. The data looked identical afterward. The difference was invisible until someone asked why the money never appeared.

The cause: the office path called a shared completion core that emits a canonical `job.completed` event; the field path wrote the status straight to the table. The billing handler listens for the event, not the column. So one caller fired the whole downstream chain and the other quietly skipped it.

## The rule: one core, many thin callers

When the same logical change can originate from more than one place, **exactly one function should own that change** — perform the write, enforce the invariants, and emit the event. Every surface that wants to make the change calls that core:

- the back-office UI route is a thin wrapper over the core,
- the agent / MCP tool is a thin wrapper over the core,
- the field or portal route is a thin wrapper over the core.

None of them touch the table directly. Convergence is the point: a single place to read to know what *really* happens on a mutation, and a single place that guarantees the event is emitted every time.

## Why "it writes the same row" is a trap

A second entry point that reproduces the database write looks correct in every way a human or a unit test usually checks: the row ends up in the right state. What it skips is everything that hangs off the *event* — invoicing, notifications, activity feed, downstream automations. Those are exactly the effects that don't show up in a quick glance at the record, so the divergence survives review and ships.

> Two callers that write the same row are not equivalent if only one of them emits the event. The row is the same; the consequences are not.

This is a sibling of a related failure — [a handler nothing subscribes to does nothing](/docs/engineering-notes/wiring-an-event-to-its-handler). There, the event fires and no consumer is wired. Here, the consumer is wired correctly but one caller never fires the event. Both end in the same place — a side effect that should have happened and didn't — so both deserve the same defense: test the real path end to end, not the function in isolation.

## Lock it with a structural test

Convergence erodes the moment someone adds a fourth caller in a hurry. The cheap guard is a structural test that greps the route tree and **fails the build if any handler mutates the protected entity outside the core**. Write it as a glob over the routes, not a hand-maintained list of four files — the list is what rots; the next inline write lands in a file nobody added to it.

Pair that with one integration test per transport that drives the *real* caller and asserts the downstream effect — complete via the field path, assert the invoice drafts; complete via the office path, assert the same. If a transport regresses to a direct write, its integration test goes red for the right reason.

## The thread

The same discipline that keeps an agent tool honest — [test the tool the way the agent calls it](/docs/engineering-notes/lessons-shipping-agent-tools), through its wrapper — applies to every writer of a shared entity. Don't verify that the row changed. Verify that the *consequences* of changing it happened, through each door a user can walk in by.

## Related

- [A handler nothing subscribes to does nothing](/docs/engineering-notes/wiring-an-event-to-its-handler)
- [Lessons from shipping agent-facing MCP tools](/docs/engineering-notes/lessons-shipping-agent-tools)
- [When completing a job bills the client](/docs/concepts/when-completing-a-job-bills-the-client)
