downstream-integration-and-caching

Downstream Integration, Config-Driven Routing, and Dashboard Caching

The admin service does not own its own data. It is a thin backend-for-frontend that sits in front of several downstream microservices — profile, auth, content, and job — and stitches their responses into the shapes the admin console needs. Three mechanisms make that work without hard-coding anything: a generic HTTP client that speaks the inter-service auth protocol, a routing table that is entirely injected from the environment, and a small cache in front of the one endpoint that would otherwise fan out on every page load.

ServiceClient — one client per downstream

ServiceClient is a single generic wrapper around fetch. One instance is constructed per downstream service, each carrying its own base URL and a service name used only for logging. It exposes one method, request<T>(path, options), which:

  • joins the base URL and the caller-supplied path, and appends a query string built from a plain options object (undefined values are dropped, so callers can pass optional filters without branching);
  • mints a short-lived service token on every call and attaches it as a bearer credential, so downstream services can authenticate the caller service rather than an end user;
  • sends and expects JSON by default, but falls back to reading the body as text when the response is not JSON;
  • returns a uniform { status, data } envelope instead of throwing on non-2xx, which lets repositories treat outcomes like not found as data (see the 404 handling below) rather than as exceptions.

The token is signed fresh per request and expires within about a minute, so there is no long-lived secret riding on the wire and nothing to refresh or revoke. The constructor refuses to build a client without a signing secret — a missing secret is a boot-time failure, not a runtime surprise.

Config-driven routing — nothing is hard-coded

Every path the admin service uses — both its own inbound routes and the outbound paths on each downstream service — is read from the environment through a requireEnv helper that throws if the variable is absent. This is deliberate fail-fast: the process will not start against a half-configured environment, so a missing route is caught at deploy time rather than on the first request that needs it.

The routing table stores complete paths, not a base plus relative fragments, and groups them by concern (employer ownership and verification, recruiter approval, claim requests, content moderation, job moderation, talent, dashboard). Dynamic paths that need an id are produced by small builder functions rather than string-concatenated at each call site, which keeps the id-interpolation in one place. A short list of routes (health checks) is marked public and exempt from the service-token requirement.

Repositories — anti-corruption between wire and domain

Repositories such as HttpEmployerRepository implement a domain-level interface but are backed by a ServiceClient to the relevant downstream. Their job is translation: the raw response DTO from the downstream service is mapped, field by field, into a rich domain aggregate through the aggregate's reconstitute factory — string statuses become value objects, a boolean claimed flag becomes an ownership status, timestamps become Dates. This mapping is the anti-corruption layer: the downstream's wire shape can drift without leaking into the domain, because only the mapper knows both sides.

Two conventions recur across the repository methods. List queries return both the mapped entities and the downstream pagination meta untouched, so the console can page without the admin service re-counting anything. And a 404 status is translated to null rather than an error, because the employer does not exist is a legitimate answer to findById, not a failure.

Dashboard caching — Redis with an in-memory fallback

The dashboard aggregates counts (pending verifications, unclaimed employers, pending/rejected recruiters, pending claims) that would otherwise require a fan-out across downstream services on every load. Those counts are cached behind a single DashboardCache interface (get / set / invalidate) with two interchangeable implementations:

  • RedisDashboardCache stores the whole stats object as JSON under one key with a short TTL (default 60 seconds). Every Redis operation is wrapped so that a cache failure degrades to a miss: get returns null on error, set and invalidate log a warning and move on. A dead cache therefore slows the dashboard down but never breaks it.
  • InMemoryDashboardCache implements the same interface with a single in-process slot and time-based expiry — the fallback when no Redis is available (local development, or a deployment that chooses not to run one).

The short TTL is the whole design point: dashboard numbers are allowed to be a minute stale in exchange for not fanning out on every request. invalidate exists so a mutation that obviously changes the counts can clear the cache immediately instead of waiting out the TTL.

Why these fit together

The four pieces form one pattern: the admin service holds no state of its own, reaches everything through configured paths and one authenticated client, translates each downstream shape into the domain at the repository boundary, and caches only the expensive aggregate read. Adding a new downstream call is uniform — declare its path in the environment, call it through the matching ServiceClient, and map the response in a repository.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →