The profile service owns a teacher's canonical data; the talent service owns the search index the school side queries. The talent-search-sync-queue node describes the producer end — how a profile edit gets enqueued. This node is the consumer end: how the talent service picks a job up off that queue and turns it into a row in the search index. The two ends meet at one Redis-backed BullMQ queue named talent-sync.
The worker: consuming talent-sync
TalentSyncWorker wraps a BullMQ Worker bound to the talent-sync queue over a Redis connection. It runs with a concurrency of 5, so several talent updates can be indexed in parallel. Each job carries a TalentSyncJobData payload — the search-relevant projection of a profile: identity (id, username, headline, bio, avatar), the facets the school side filters on (skills and their ids/names, languages, city and country, preferred locations, availability, experience level and years, a salary-expectation range, credentials), and the visibility fields (a public/limited/hidden level plus per-audience booleans).
The worker itself does almost nothing beyond translation: it logs the job in, maps the payload to an UpsertTalentCommand, awaits the handler, and logs the job out. It attaches completed and failed listeners for observability, and exposes a close() for graceful shutdown. Because it awaits the handler, a throw propagates back to BullMQ as a job failure — which is what lets the queue's retry policy (defined on the producer side) actually do its job.
From job to command: the mapping
The mapping step is deliberate, not a passthrough. username on the wire becomes displayName on the command. Nullable wire fields — avatarUrl, yearsExperience, salaryExpectationMin/Max — are normalized from null to undefined (?? undefined), so the downstream layer sees "absent" rather than an explicit null. Skills arrive in three parallel shapes (skills, skillIds, skillNames) and are carried across as-is. This is the seam where the profile service's vocabulary and the search service's vocabulary are reconciled in one place.
The handler: visibility decides upsert vs. delete
UpsertTalentHandler.execute is the single write path into the index. It first guards its input — a command with no id throws immediately. Then it branches on visibility, and this is the load-bearing decision:
- If
visibility === 'hidden', the talent is deleted from the search index rather than written. Hiding a profile is expressed as an index removal, so a teacher who goes hidden simply stops being findable. - Otherwise, a
TalentDocumentis upserted. Upsert (not insert) is what keeps the index eventually consistent — the same code path handles a brand-new teacher and the hundredth edit of an existing one.
The handler comment names the destination in as many words: the document it builds is the one for Meilisearch, which is the search engine behind the talent surface.
Derived fields at index time
The handler does not just copy the command into a document — it derives several fields so the index carries answers the raw profile does not:
displayNamefalls back throughdisplayName → fullName → 'Unknown', so the index never stores a nameless row.hasVerifiedCredentialsis computed as "does any credential have statusverified" — a single boolean the search side can filter on without walking the credential array.publicToEmployersis true when visibility ispublicorlimited;publicToRecruitersis true only whenpublic. So the one visibility level fans out into per-audience access flags right at write time, and the index enforces who may see whom.experienceYearsis written as an alias ofyearsExperiencefor filter compatibility, andupdatedAtis stamped with the current time on every write.
The shape of the design: the profile is the source of truth, the index is a derived view, and the derivation lives in one handler so every talent row is built the same way.
Around the pipeline
Two more pieces of talent-service infrastructure sit beside the sync path. ProfileServiceClient is the reverse direction — an authenticated service-to-service HTTP client the talent service uses to read back from the profile service: an employer's verification status, a recruiter's approval status, and the caller's own talent profile. It passes the caller's session through and, on any error or non-success response, logs and returns null rather than throwing — a read failure degrades to "unknown," never a crash. An interface is extracted so tests can supply a mock.
TalentCleanupService is a small opportunistic janitor: at most once per hour, it fires a non-blocking background pass (its own failures are caught and logged, never surfaced to the caller) that deletes expired invitations older than 90 days. It is the genuinely fire-and-forget part of the service, kept apart from the awaited sync write.