Talent Sync to Search Service
The profile service owns the teacher's canonical data. The talent service owns the searchable index the school side queries. These are two separate services with two separate stores, so a profile edit has to propagate — when a teacher changes their headline, skills, or who they're visible to, that change needs to reach the search index. This node is about the seam that carries it across.
One port, two ways across
The application layer never talks to a queue or an HTTP client directly. It depends on a single port, TalentSyncPort, whose whole surface is one method: sync(data). The port's own comment states the intent plainly — the implementation may be synchronous (HTTP) or asynchronous (queue). So the domain says only "this talent's data changed, get it to search"; which road that takes is an infrastructure choice, swappable without touching the use cases.
The payload it carries, TalentSyncData, is the search-relevant projection of a profile: identity (id, username, headline, bio, avatar), the facets the school side filters on (skills, languages, city/country and preferred locations, availability, experience level and years, a salary-expectation range, credentials), and — importantly — the visibility flags: a public/limited/hidden level plus three booleans for whether the teacher is public to schools, to employers, and to recruiters. Visibility travels with the sync, so the index can enforce who is allowed to see whom.
The queue road: BullMQ over Redis
BullTalentSyncAdapter is the asynchronous implementation. It publishes onto a BullMQ queue named talent-sync backed by Redis, and its job options are the interesting part — they're the durability contract:
- 3 attempts with exponential backoff starting at a one-second delay, so a transient failure downstream retries on its own rather than dropping the update.
- Remove on complete, so a healthy queue doesn't accumulate finished jobs.
- Keep on failure — a job that exhausts its retries is not discarded. It stays for inspection, so a sync that genuinely failed is visible rather than silently gone.
Each job gets an id shaped like talent-<id>-<timestamp>, and publishing is logged on the way out. The talent service is the consumer on the other end of this queue; the profile service's job is only to enqueue.
The Redis connection itself is a lazily-created singleton client, configured from an environment variable, with its own bounded retry: a few attempts with a capped, increasing delay before it gives up on a single request. So there are two retry layers stacked — the connection retries the transport, the queue retries the work.
The HTTP road: best-effort, non-blocking
TalentServiceClient is the synchronous alternative — a direct outbound call that POSTs the talent record to the search service's upsert endpoint, going out as an authenticated service-to-service request. Its defining trait is what it does on failure: nothing that stops the caller. A 4xx/5xx response is logged; a thrown network error is caught and logged. Neither is re-raised. The code says why in as many words — sync failures should not block the main operation. Saving the profile is the thing that must succeed; getting it into the search index is desirable but subordinate, and is allowed to fail loudly-in-the-logs but quietly-to-the-user.
The design underneath
The two roads express one consistent stance: the profile is the source of truth, the index is a derived view, and keeping them in step must never hold the profile hostage. The queue road buys durability and automatic retry for that catch-up; the HTTP road buys immediacy but degrades to best-effort. Because both sit behind the same one-method port, the profile service picks its trade-off at wiring time, and the domain stays ignorant of which one is live.