Decisions · ADR-004

View / mutation type separation

Status
Accepted
Date
Surfaces on

Read endpoints can only call read-tagged functions; write endpoints can only call mutation-tagged functions. Compile-time refusal of code that crosses the boundary.

Context

A single shared service layer where read and write paths intermingle creates the risk of silent state changes during view operations. A naive bug, say a “find or create” pattern that creates when it shouldn’t, can write to the database during what the caller believes is a read. After the fact, this is undetectable; the audit log records the mutation but the calling context says “I was just reading.” The platform’s evidentiary posture depends on read operations being read-only.

Decision

The codebase enforces a typed split at the route layer: read endpoints can only call functions tagged readonly; write endpoints can only call functions tagged mutation. There is no shared layer that does both. Static analysis rules plus the type system reject code that crosses the boundary. The compile fails before the bad code can ship.

Consequences

Silent state changes during view operations are architecturally impossible. Not because we wrote a policy against them, but because the type system refuses to compile code that does it. Refactoring across the boundary is explicit (you change a function’s tag, which forces every caller to acknowledge the new shape). Slightly more boilerplate at the function-tag level. The discipline applies uniformly across the entire service layer; new code follows the pattern automatically.

Alternatives considered

  • Runtime checks for mutation in view context. Rejected: undetectable post-hoc; doesn’t catch at compile. Bugs ship and the audit log fills with mutations that “shouldn’t” have happened.
  • Code review as the enforcement mechanism. Rejected: relies on humans noticing; fails on the busy day. Compile-time enforcement is the only mechanism that scales.

References

  • standard Command-Query Responsibility Segregation (CQRS) pattern
  • standard Type-driven design as a basis for security-relevant invariants