Appendix B - 21. User-journey model (product-goal → implementation bridge)

The Structure of User-journey model (product-goal → implementation bridge) — its shape at a glance:

Each journey is an actor pursuing a goal through ordered steps, each step naming the endpoint it crosses a boundary to reach. A call-site drift lint holds the derived dependency list to the real calls in both directions.

flowchart LR
  Actor([Actor]) --> J[Journey: goal]
  J --> S1[Step 1] --> S2[Step 2]
  S1 -->|calls| E1[/endpoint/]
  S2 -->|calls| E2[/endpoint/]
  E1 -.->|declared ↔ real| Drift{{Call-site drift lint}}

Accessible description: an actor pursues a goal through ordered steps, each step calling an endpoint. A call-site drift lint reconciles the journey's declared dependencies against the endpoints its code actually calls, both directions.

Projected from the catalogue entry system-models / User-journey model (product-goal → implementation bridge).

On this page: Intent · Motivation · Applicability · Structure · Sample Code · Consequences · Example use within DocAble · Related Patterns

Intent

Intent — Model the product's user journeys as first-class typed entities: each an actor pursuing a goal through ordered steps, every boundary-crossing step joined to the endpoint it calls. The path from what the product is for to what the code must provide becomes a queryable model. Graft it into the service-architecture dialect you already lint and query rather than standing up a parallel model (our instance: a UML-use-case-flavored Journey entity kind added to the existing Backstage service-flow dialect).

Motivation

The path from a product goal (a user opens a document in the editor, applies fixes, re-checks conformance) to the code that serves it lives in people's heads. Nothing ties a journey to the endpoints it hits, the tests that cover it, or the services it needs. So journeys go untested without anyone noticing; endpoints accrete that no journey needs and no one dares delete; a component's declared dependencies quietly fall out of date with the calls its code actually makes; and the backend is sized for a blurred average of all traffic rather than the journey underway. Each is a quiet failure: a coverage hole, dead surface, a stale dependency map, mis-sized capacity. None is visible from the code alone, because the journey is the one thing the code never names.

Applicability

Structure

The Structure diagram appears at the top of this page.

Sample Code

A journey's dependency list is derived from its real call sites, so a drift lint checks it both ways: every declared dep has a real call, and every call is declared. This makes the model a checked cache of the call-site truth, not a hand-authoritative document that rots.

import sys

def call_site_drift(declared: set[str], actual_calls: set[str]) -> list[str]:
    """Both directions: no stale declaration, no silent under-declaration."""
    findings  = [f"declared dep '{d}' has no call site (stale)" for d in sorted(declared - actual_calls)]
    findings += [f"call to '{c}' not in declared deps (under-declared)" for c in sorted(actual_calls - declared)]
    return findings

def dead_endpoint_audit(all_endpoints: set[str], reached: set[str]) -> list[str]:
    """Every exposed endpoint must be reached by some journey; the rest is dead surface."""
    return [f"endpoint '{e}' reached by no journey" for e in sorted(all_endpoints - reached)]

if __name__ == "__main__":
    # `declared_deps` reads the journey's model; `scan_calls` reads its code's service-client calls.
    findings = call_site_drift(declared_deps(), scan_calls())
    for f in findings:
        print(f"JOURNEY-DRIFT: {f}")
    sys.exit(1 if findings else 0)

Consequences

Example use within DocAble

Contents
© James C. Davis, 2026–present