Service Architecture and Request Flow (youteacher_job)
youteacher_job owns every job record the YouTeacher network exposes — whether it came from a direct employer posting or from the external scrapers maintained by the job_scrapers project. Downstream surfaces (the web front end, notifications, search) treat this service as the single source of truth. It is a Fastify-based app organised with the classic DDD layer stack, and its most instructive quality is how strictly the layers stay apart while a single request threads through them.
Four layers, dependencies pointing inward
The src/ tree splits into four layers, and the same rule holds as in the front end: dependencies only ever point inward.
interfaces/— Fastify controllers. They speak HTTP, parse and validate input, enforce rate limits, and translate the outcome back into a response. They know nothing about how data is stored.application/— use cases (called handlers here), the repository interfaces they depend on, DTOs, and application services (caching, search, validity, change notification). A handler orchestrates; it depends on an interface such asJobRepository, never on a concrete Prisma class.domain/— entities, value objects, and business rules, with no framework imports.infrastructure/— the adapters that implement the application's interfaces against the real world: Prisma repositories, Redis adapters, the queue provider, the search client, and HTTP clients to sibling services.
Relative parent imports are banned by lint (everything reaches across the tree through @/... aliases), which keeps a refactor from silently re-pointing a dependency the wrong way.
Two processes from one image
The service ships as a single container image that runs in one of two shapes:
- the Fastify HTTP API, the request-serving process; and
- the Bull ingest worker, a background process that subscribes to the shared job-updates queue, normalizes scraper payloads, and writes them through.
Both share the same code and configuration. The worker fulfils the "while(true) loop" expectation without a hand-written loop: it registers a handler with Bull's Queue.process, and Bull keeps pulling jobs as long as the process is alive, with retry and backoff configured on the queue.
Bootstrap: build the parts, then wire them
Startup is deliberately staged, and each stage is a small pure function so the same wiring runs in tests as in production.
bootstrapDatabaseensures the tables exist, then constructs the concrete Prisma repositories (jobs, job reports, saved jobs) and returns them.createHandlerstakes those repositories plus the application services (search, caches, change notifier, validity service) and constructs every use-case handler, injecting exactly the collaborators each one needs — for example,CreateDirectJobHandlergets the job repository and the change notifier, whileGetJobDetailHandlergets the repository, the cache, and the validity service.createAppbuilds the Fastify instance, registers CORS and the service-auth plugin, assembles the controller dependencies (handlers, geocoding, an auth helper, the rate limiter, and default rate-limit config), and then hands everything to the controller registration.
There is no DI container. The bootstrap functions are the composition root: they are the only place that knows both an interface and its concrete implementation, so the layers below stay ignorant of Prisma, Redis, and Fastify.
Routes are configuration, not literals
A distinctive choice: no route path is hard-coded. config/routes.ts reads every path from an environment variable through a requireEnv helper that throws if the variable is missing, so a misconfigured deployment fails loudly at boot rather than serving a wrong URL. The same module also declares which routes are public (no user auth) and derives parameterised paths (like a posting detail URL) by substituting the id into the configured template. Every production path is versioned so the service can iterate without breaking the front end.
A URL-rewrite hook on the Fastify instance lets callers omit the API base prefix: internal, health, and already-prefixed URLs pass through untouched, and everything else is prefixed automatically.
One request, end to end
The controller layer is itself split by concern. A single registerJobsHttpController composes four sub-controllers — postings, search (which also carries the report endpoint), saved jobs, and admin — handing each only the handlers and rate-limit slices it needs. A request then flows:
- Route → controller. Fastify matches the configured path and calls the controller.
- Controller guards. It applies the rate limit for that endpoint (before validation, on the appropriate scope — an authenticated identity for posting, a client IP for anonymous endpoints), validates the payload, and resolves the caller when auth is required.
- Controller → handler. It calls the matching application handler with a clean, validated command or query.
- Handler → repository / services. The handler runs the business logic against the repository interface and the application services, never touching Prisma or Redis directly.
- Response. The handler returns a normalized
JobDTO(or the relevant result), which the controller serializes. Controllers only ever throw a small set of typed errors, each mapping to a structured{ message, code }body, and rate-limit rejections carry a retry hint.
Reads and writes stay in lockstep with the cache and the search index because those updates happen inline inside the handlers and services rather than through a second queue: after a write lands in Postgres, the same code path refreshes the Redis detail cache and the search document synchronously.
A note on service-to-service auth
At the architecture level, calls between internal services are authenticated by a signed service token carried on the request, verified by a Fastify plugin registered at app startup; an auth helper resolves the calling user for endpoints that need one. This is explicitly a stopgap until a dedicated auth service issues scoped tokens. The concrete signing scheme, secret handling, and token contents are deployment secrets and are intentionally out of scope here.
Related
- youteacher_job — the job service overview
- ddd-hexagonal-architecture — the same layering discipline on the web front end