ADR-0045: Facade, Proxy, and Unit of Work Patterns¶
- Status: Accepted
- Date: 2026-04-18
- Relates to: ADR-0017 (ActiveGraph ORM), ADR-0019 (App Surface), ADR-0021 (Progressive Enhancement), ADR-0023 (SPARQL Federation)
Context¶
Trails has a rich set of modules — decorators, ORM, shapes, runtime, HTTP adapter, federation — but wiring them together requires touching 5+ imports and understanding internal sequencing. New users face a "wall of imports" before they can run a hello-world. Three well-known design patterns address distinct friction points:
-
Facade (App class). Users must import and compose
@capability,@node_type,@shape,Context,create_http_appseparately. A singleAppentry point hides this wiring. -
Proxy (FederationPeer). The federation surface currently connects to remote peers eagerly when configured. Peers that are down at startup cause errors even if no federated query is ever issued. A lazy proxy delays connection and schema fetching until first use.
-
Unit of Work.
ctx.kg.transaction()already buffers SPARQL writes at the KG level. A higher-levelUnitOfWorkoperates on theContextlevel: it stages model saves and deletes, then commits them as a single SPARQL UPDATE. This gives application code a cleaner transaction boundary that works with ORM instances rather than raw SPARQL.
Decision¶
Facade — trails.app.App¶
App(name, version=, base_iri=)— single constructor.@app.model(name, fields=)— registers a@node_type.@app.capability(name, description=)— registers a@capability.@app.shape(name, predicates=)— registers a@shape.app.run(host=, port=)— starts the HTTP adapter via uvicorn.app.context()— returns a freshContext.
App delegates entirely to existing modules. It adds zero new
functionality — it is a pure Facade over trails.decorators,
trails.orm, trails.shapes, trails.runtime, and
trails.http_adapter.
Proxy — trails.federation_proxy.FederationPeer¶
FederationPeer(name, url, mcp_url=)— stores config, does NOT connect..schemaproperty — fetches and cachesSchemaAdvertisementon first access viafetch_peer_schema()..healthyproperty — probes health on first access, caches result..query(sparql, trace_id=)— executes a federated SPARQL query..invoke(capability_id, arguments)— invokes a remote capability via MCP..refresh()— clears cached schema and health for re-probing.reprshows status:lazy,connected, orunreachable.
Unit of Work — trails.uow.UnitOfWork¶
UnitOfWork(ctx)— wraps aContext..save(instance)/.delete(instance)— stage operations..execute_sparql(sparql)— stage a raw SPARQL UPDATE..commit()— execute all pending operations in one SPARQL UPDATE..rollback()— discard all pending operations.- Context manager: commits on clean exit, rolls back on exception.
This is intentionally distinct from ctx.kg.transaction() which
operates at the KG/store level. UnitOfWork is the application-level
pattern that collects model mutations and lowers them to a single
KG transaction at commit time.
Consequences¶
- Positive: Dramatically simpler onboarding —
from trails import Appis all a new user needs. Federation peers are safe to configure even when peers are offline. Application code can batch complex multi-model mutations atomically. - Negative: One more layer of indirection. Users who outgrow
Appmust learn the underlying modules.UnitOfWorkadds a second transaction surface alongsidectx.kg.transaction()— docs must clarify when to use which. - Neutral: All three patterns are opt-in. Existing code continues to work unchanged.