Case study 01

Recruit 360 — one vacancy, three channels, one candidate record

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

The system

Recruit 360distribution & identity

Diagram: one vacancy record is published to three channels — PNet over REST, Career Junction over FTP, and an embeddable portal via iframe — and 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

Three channels, three schemas, one identity. PNet confirms a posting synchronously; 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 — 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, 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.

Recruit 360 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.

Distribution: 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, 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 resolved into the same user-facing feedback.

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 that re-checked vacancy status across channels and corrected the record, so drift never survived longer than a day, and an on-view check that re-synced status whenever a recruiter opened a vacancy in the back end, so the thing in front of them was current rather than merely recent.

Publishing and republishing

Vacancies get edited, and they get reopened months later. The publishing system distinguished between the two: 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.

Identity: one person, many companies

The part of the data model I am most pleased with. A candidate signs up once. If they later apply to a second company using the platform, they are 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 — email address, phone number, ID number where we did hold it, and name and surname — with the signal set varying by 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 — a considerably worse failure than 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 would do differently

Recruit 360 is where I learned to build software, and it shows in the early code. I would 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 would 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.