Ephemeris5 MIN READ08 / 31

Provider Router — High-Level Design

Reading guide: The current package architecture defines the binder boundary. Sections below describe reference designs unless an implementation note says otherwise. Standalone facade, cache and provenance services are dropped; their integration responsibilities belong to the consumer.

Status: Draft for review · Part of: Astro Binder · Updated: 2026-10-24

The router turns a canonical request into exactly one provider decision: a provider, a reason and a cache key, or a typed error when nothing can serve. It holds no results and calls no provider.

The decision boundary is implemented in the standalone @astrology/provider-router package. It uses injected capability snapshots, a host-resolved TT instant, explicit coverage-fallback policy and read-only cache/breaker/quota hooks. Concrete cache storage, breaker state machines, quota counters and the calculation facade remain future modules. The architecture below remains the broader reference design.

1. Purpose

Two providers with different strengths force one decision, and the product must not make it per feature. Otherwise “the same chart” means different numbers in a natal and a transit view, with no record of which engine answered.

The router decides once:

  • capability — can any provider serve this feature, body and zodiac?
  • eligibility — do coverage, breaker state and quota allow it now?
  • preference — Swiss Ephemeris by default; JPL Horizons only by declared fallback.
  • gates — is the answer cached, and is the chosen provider healthy?
  • decision — one provider, a reason, a cache key, or a typed refusal.

2. Where it sits

canonical request ──▶ provider router ──▶ provider decision
runtime state ──────▶        │
                            ▼
                    facade calls the chosen adapter
  • Upstream: the calculation facade, with a validated canonical request.
  • Reads: the capability matrix, breaker registry, cache and engine versions.
  • Downstream: the facade calls the adapter the router names.
  • Never: calls an adapter, normalises values, caches results or derives astrology.

3. Responsibilities

Stage Owns Refuses to do
Match capability Feature × body × zodiac from the capability matrix Route a feature no provider declares
Filter eligibility Per-provider coverage, breaker state and quota Allow a provider whose files do not cover the instant
Pick primary Swiss first when eligible; Horizons only where a coverage fallback is declared Mix providers inside one response
Check runtime gates Cache key (provider + request + engine version), final breaker check Hold or return results
Return decision Provider id, reason, cache key Call the adapter or hide the reason

4. Architecture

Provider router — high-level design Reference design. The router turns a canonical request into exactly one provider decision, using the capability matrix, coverage, primary preference, cache key and breaker state. It holds no results and calls no provider. Provider Router — High-Level Design Capability, coverage and health narrow to one provider per request. No results are held here. Canonical request feature · bodies · instant zodiac · observer Runtime state breaker · quota · health engine versions Provider router · policy only Match capability feature × body × zodiac no capability, no call Filter eligibility coverage · breaker · quota remove what cannot serve Pick primary Swiss first when eligible Horizons for fallback only Check runtime gates cache key · breaker state skip the call or drop it Return decision provider · reason · key or PROVIDER_UNAVAILABLE Capability matrix feature × body × range precision · engine version Breaker registry per provider and endpoint open state blocks selection Provider decision provider · reason · cache key or PROVIDER_UNAVAILABLE router selects · facade calls · one provider per response dependency module result

Diagram: reference design, not deployed behaviour.

The router is a policy component, hence no provider cards in the diagram: the facade calls adapters after the decision, never the router.

5. Rules

  1. One provider per decision. A response carries one provenance block, so the router returns one provider or an error, never a partial or merged answer.
  2. Swiss is the default. Horizons is reachable only through the declared coverage fallback, for a body or date Swiss files cannot serve. A Swiss outage is PROVIDER_UNAVAILABLE, not a silent substitution.
  3. Narrow in order. Capability, eligibility, preference, runtime gates: the diagram’s stage order, so an early failure never touches later state.
  4. Coverage is checked, not assumed. A provider that cannot serve the instant leaves the candidate set, even when healthy and preferred.
  5. The cache key is part of the decision. Provider id, full canonical request and engine version: a hit needs no adapter call, and an upgrade invalidates old entries by construction.
  6. Deterministic. The same request and runtime state give the same decision and reason string.
  7. Stateless apart from policy. No results or provider payloads are stored; only breakers and counters survive a request.
  8. Houses are Swiss-only. The matrix encodes that, so the router refuses rather than approximating a house system on Horizons.

6. Edge cases

Case Behaviour
Body served by both, instant outside Swiss files Horizons if the fallback is declared for that body; otherwise OUT_OF_EPHEMERIS_RANGE
Instant inside Swiss files, Moshier fallback in use Still Swiss, flagged degraded by the adapter and normalizer
Horizons breaker open Validation and fallback cases are marked unavailable; user requests are unaffected
Cache entry from an older engine version Treated as a miss; the key includes the engine version
Unknown feature or house system Typed error before any capability lookup
Both providers ineligible PROVIDER_UNAVAILABLE with per-provider reasons in the error detail
Breaker trips between selection and call The gate re-checks; the decision fails closed rather than calling a known-bad provider
Request for a body outside every provider’s range OUT_OF_EPHEMERIS_RANGE, not an empty position list

7. Out of scope

  • Calling adapters, parsing provider responses and retrying transport failures.
  • Unit, frame and zodiac normalisation and provenance assembly (normalizer).
  • Astrology semantics and chart composition.
  • Handling a degraded answer beyond declaring it (the facade owns the response).

8. Planned code

Piece Responsibility
router/ProviderRouter Stage orchestration and the decision object
router/CapabilityMatrix Feature × body × zodiac × range per provider
router/EligibilityFilter Coverage, breaker and quota filtering
router/PrimarySelector Swiss-first preference and declared fallback rules
router/CacheKeyFactory Provider + canonical request + engine version
router/BreakerRegistry Per-provider and per-endpoint breaker state