25
Backend
04
Security
08
iOS
07
Infra

Route every modification write through one apply-and-rebuild service method

ADR-0065 ACCEPTED · 2026-04-11
Route every modification write through one apply-and-rebuild service method

Decision

Apply a modification to a trip through one service method, whatever the transport. Inside one transaction it inserts the modification under the trip lock (ADR-0051), rebuilds the trip from the stored history and refreshes the trip's denormalized purpose column. After commit it publishes the change signal (ADR-0041) and runs post-commit hooks, then returns the rebuilt trip with the new position and head. GraphQL resolvers, MCP tools and background enrichment build a modification and hand it over, and none of them know the steps.

Two writes call the lower-level insert directly because they must share a transaction with another row. Creating a trip inserts the trip and its empty first modification together, and recording an uploaded document inserts the document row and its effect together.

Why

Every change to a trip takes the same steps: store a modification in the log (ADR-0033), load the history, rebuild the trip and publish a signal. When MCP arrived beside GraphQL, each resolver and each tool ran those steps itself. Small differences in naming and order hid how much was duplicated. Some resolvers also returned a trip computed in memory before the store, which could differ from what was committed.

Rejected alternatives

  • Each transport running the steps itself. Every copy has to repeat the steps in the right order, and any of them can store without publishing, or without rebuilding.
  • Returning a trip computed in memory before the write. Under concurrent edits it can differ from what was committed, and the caller gets a trip that doesn't exist.

Consequences

A new transport calls the same method and inherits every step. A rule that must hold for every write goes into that method. The two direct inserts sit outside it, so the uploaded-document path publishes its own signal, and a new write that bypasses the method leaves the purpose column stale.