architecture-and-request-lifecycle

The youteacher_admin service is the operator-facing backend, and it is laid out as a hexagonal (ports-and-adapters) application with four concentric layers. The dependency arrows all point inward: outer layers know about inner ones, never the reverse.

  • interfaces — the Fastify HTTP surface: createApp, the AdminHttpController route registrar, and plugins.
  • application — use cases, one per operation family (e.g. EmployerVerificationUseCase). They orchestrate; they hold no framework and no transport code.
  • domain — aggregates and value objects (Employer, VerificationStatus, OwnershipStatus) plus the repository interfaces the application depends on. This is the layer that owns the rules.
  • infrastructure — the adapters that satisfy those interfaces: HTTP repositories, a ServiceClient, auth helpers, an in-memory cache and event bus.

Wiring: bootstrap, then create the app

Construction is a pure function. bootstrapService(config) takes the downstream service URLs plus a shared JWT secret and, in one pass, builds:

  1. one ServiceClient per downstream service (profile, auth, content, job), each stamped with callerService: "admin";
  2. non-request-scoped infrastructure — a dashboard cache and an event bus, each defaulting to an in-memory implementation;
  3. the auth stack (AuthSessionClientUserAuthHelper);
  4. the HTTP repositories and HTTP services, each handed the client it talks through;
  5. the use cases, each handed the domain interfaces it needs.

The result is a plain deps bag. createApp({ jwtSecret, deps }) then registers Fastify plugins and calls registerAdminRoutes(app, deps). Nothing constructs its own dependencies — everything is injected from the one bootstrap pass, which is what keeps the domain and application layers free of transport concerns.

A request, end to end

  1. URL rewrite. Fastify's rewriteUrl prefixes every incoming path with the API base, except health, env-check, and internal (/__) paths, so handlers can be written against clean paths.
  2. Request context. An onRequest hook captures the inbound session cookie into @fastify/request-context, so repositories can forward the caller's identity downstream without threading it through every function.
  3. Auth. A service-auth plugin runs, then a second onRequest hook in the controller enforces admin authentication on every route — public routes (health) and the diagnostic env-check are the only exceptions. Non-admins never reach a handler.
  4. Handler → use case. The route handler unpacks the request and calls a single application use case.
  5. Use case → domain. The use case loads an aggregate through a repository interface, invokes a domain method that enforces the rule (e.g. an employer can only be verified from the pending state), then asks the repository to persist the new state.
  6. Repository → downstream. The HTTP repository reads the forwarded headers from request context, calls the profile service through its ServiceClient, and maps the raw response back into a domain aggregate via reconstitute. A 404 from downstream becomes a null, which the use case turns into a not-found error rather than a crash.
  7. Response / errors. A central error handler translates typed errors into HTTP: a domain/HTTP error carries its own status and code, a validation error becomes 400, and anything else becomes a generic 500 — so no stack trace leaks to the caller.

The shape repeats for every domain (recruiter, quota, talent, claim, content, job, dashboard): handler → use case → domain aggregate → HTTP repository → downstream service. The service holds no database of its own; it is an orchestration and policy layer in front of the profile, auth, content, and job services.

Why it's built this way

The domain rule lives in exactly one place — the aggregate — and the application layer only sequences calls, so the transport (HTTP-to-downstream) is a swappable adapter behind a repository interface. Header forwarding through request context, rather than through argument lists, keeps that seam clean: a use case never sees a cookie or a JWT.

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 →