Case study 04

A recruitment platform that posts one vacancy to three channels and keeps one candidate record

Role
Core platform developer
Employer
Idealweb · Jan 2016 to May 2019
Stack
Laravel · PHP · MySQL · JavaScript / jQuery · Bootstrap · REST · FTP
Shown
This case study. The platform belongs to my former employer, so no code or screenshots.

The system

Recruitment platformdistribution & identity

Diagram of one vacancy record published to three channels. PNet over REST, Career Junction over FTP, and an embeddable portal via iframe. Applicants from all three resolve back to a single candidate record. Select a channel to read how it confirms a posting.

vacancy.create()1 record
candidate.resolve()1 person · n companies

Each channel has its own schema. PNet confirms a posting synchronously, and Career Junction only by moving a file. The recruiter sees one plain status either way, and every applicant resolves back to one record.

The problem

Recruitment teams, everyone from two person agencies to corporate HR departments, were running the same job in several places at once. A vacancy went up manually on PNet, again on Career Junction, and again on the company's own careers page. Each channel returned its applicants separately. Three inboxes, three spreadsheets, and no single view of who had applied to what.

The duplication was not the real cost. The real cost was that the same person could appear as three different candidates, and a recruiter had no reliable way to know it.

The platform was the system of record underneath all of it. Post once, distribute everywhere, and pull every applicant back into one candidate database a recruiter could actually work from.

Constraints

  • Two job boards, two entirely different integration models. Not just different fields, a different idea of what "confirmed" means.
  • A third channel we controlled, an embeddable careers portal that had to render inside a client's own website via iframe, on sites we had no access to.
  • Multi tenant from day one. Every client company had its own candidates, vacancies, branding and users, with strict separation between them.
  • Limited identity data. Neither board would pass us applicants' ID numbers, which shaped the whole approach to matching.
  • Non technical users, recruiters and HR administrators. Anything that needed explaining became a support ticket, and I was the person answering those tickets.

Two boards, two definitions of success

Both integrations were built against the boards' functional documentation, with validation layered on top so a vacancy was checked against each board's rules before it was ever sent.

The interesting part was that the two boards disagreed about how you find out whether a posting worked. PNet answered synchronously. Post the vacancy, get a response back, and know immediately whether it was accepted. Career Junction answered by moving a file. Postings were submitted over FTP, and acceptance was signalled by the file moving out of the folder it had been placed in. There was no response to read. You had to go back and look at the filesystem to find out what had happened.

So one channel gave a definitive answer at the moment of posting, and the other required observation after the fact. Either way, the recruiter needed to be told plainly whether their job was live, so both paths ended up giving them the same plain answer.

Keeping state honest

The platform's view of a vacancy could drift from the boards' actual state, and we found that out in production. The fix was two reconciliation paths. A nightly cron rechecked vacancy status across every channel and corrected the record, so drift never lasted longer than a day. And a check that ran whenever a recruiter opened a vacancy in the back end resynced the status too, so what they saw was current rather than merely recent.

Publishing and republishing

Vacancies get edited, and they get reopened months later. The publishing system handled both differently. An update could be pushed to the existing live posting, or a vacancy could be published as a separate new posting when a role was genuinely being reopened.

One person, many companies

The part of the data model I'm most pleased with. A candidate signs up once. If they later apply to a second company using the platform, they're associated with that company rather than recreated as a new record. So applications arriving from PNet, Career Junction and the portal all resolved back to one candidate identity.

Matching had to work without ID numbers, since neither board would share them. It worked across the signals we actually had, which were email address, phone number, ID number where we did hold it, and name and surname. Which signals we used varied depending on where the application came from.

The system flagged rather than merged. Suspected duplicates surfaced on the candidate view as links to the other matching records, and the recruiter decided. That was deliberate. On a fuzzy match without a strong identifier, an automatic merge collapses two real people into one record in a system that feeds hiring decisions. That's a considerably worse failure than just showing a recruiter two records and asking. Recruiters could also set a not_duplicate flag from a single button on that view, which overrode the checks from then on.

Automatic merging with recruiter override was on the roadmap when I left. Given the data available, flagging first was the right order to build it in.

Candidate records and generated CVs

Recruiters could add candidates directly, and the system flagged incomplete profiles and let them request the missing information from the candidate rather than chasing it by email. Because the data was structured rather than sitting inside an attached document, a CV could be generated from it in the client's own branded template.

What I'd do differently

This is where I learned to build software, and it shows in the early code. I'd write it DRY from the start. A lot of the early functionality was static and repetitive, and I ended up refactoring substantial parts of it while simultaneously building new features on top.

I'd also insist on a plan. There was no real specification beyond building something better than Placement Partner, which was enough to start but not enough to build on. We worked in something close to extreme programming, with trial versions sold at a discount to experienced recruitment agencies who fed back on what they needed and we built it. The feedback loop was genuinely valuable, but without planning around it, it produced features faster than it produced coherence. I'd keep the customer loop and put an agile process around it.

If this is close to the work you’re hiring for, email me or download the CV. The rest of the work is live, so you can open it yourself.