Building Skill Align - Part 6 - Project Staffing Assistant(Backend)
Source: Dev.to
Project Staffing Assistant – SkillEvaluatorService
Project Staffing Assistant helps managers decide which candidates are suitable for a project based on actual project requirements.
Below is a cleaned‑up view of the backend implementation (the intelligence layer) written in Apex.
The Core Service – SkillEvaluatorService
public with sharing class SkillEvaluatorService {
// … implementation …
}
Two important design decisions
| Decision | Reason |
|---|---|
public | Required because LWC will call this Apex class. |
with sharing | Ensures record‑level security is respected (queries and DML respect the current user’s sharing rules). |
I had previously configured roles, OWD, and sharing rules (see the related blog post here). Using
with sharingguarantees that this evaluation logic follows those configurations.
Apex Sharing Behavior
- Apex runs in system context by default; object‑level and field‑level permissions are not automatically enforced.
with sharingenforces record‑level sharing rules only.with sharingdoes not enforce object or field permissions—you must handle CRUD/FLS explicitly (e.g.,WITH SECURITY_ENFORCEDorSecurity.stripInaccessible()).- If no sharing keyword is defined, the class inherits sharing from its caller, so behavior may vary depending on how it is invoked.
- Triggers run in system context. Even if a helper class is marked with sharing, the trigger itself executes in system mode.
Designing Data Transfer Objects (DTOs)
Instead of returning raw Employee__c or Employee_Skill__c records, I created DTOs that expose only the fields required by the UI. This keeps the response clean, prevents unnecessary data exposure, and isolates business logic in the backend.
Note:
@AuraEnabledis required for LWC to access Apex properties and methods.
Skill‑Level DTO
public class SkillGapDetail {
@AuraEnabled public String skillName;
@AuraEnabled public Integer requiredLevel;
@AuraEnabled public Integer impact;
}
Represents a single skill gap for a candidate.
Advantages
- All evaluation logic runs in Apex; the UI performs no calculations.
- Business logic stays in the backend.
- UI remains lightweight.
- Future logic changes don’t affect frontend code.
Candidate‑Level DTO
public class CandidateResult {
@AuraEnabled public String employeeName;
@AuraEnabled public Decimal gapScore;
@AuraEnabled public Boolean isProjectReady;
@AuraEnabled public SkillGapDetail detail;
}
For each evaluated employee, the UI receives:
- Employee name
- Final gap score
- Ready / Not Ready flag
- Skill‑gap detail (the DTO above)
Entry Point – evaluateProject()
@AuraEnabled
public static List evaluateProject(Id projectId, Integer topN) {
// implementation …
}
Responsibilities
- Accept a Project Id.
- Evaluate unallocated employees.
- Rank them.
- Return the top N candidates.
- Persist evaluation results.
Guard Clause
Guard clauses stop processing early when required inputs are missing, preventing unnecessary governor‑limit consumption and UI errors.
if (projectId == null) return new List();
Prevents
- Null‑pointer exceptions.
- Unexpected UI errors.
- Wasted governor limits.
Load Project Requirements
List reqs = [
SELECT Skill__c, Required_Level__c, Importance__c, Weight__c
FROM Project_Skill_Requirement__c
WHERE Project__c = :projectId
];
Each requirement contains:
- Skill
- Required Level
- Importance (Required / Nice‑to‑have)
- Weight (configurable per skill)
After fetching, I convert the list into Maps for fast, constant‑time lookups (O(1)). This avoids SOQL inside loops and keeps the code bulk‑safe.
Weighted Impact Formula
The core of the evaluation engine calculates a deficit and then applies a weighted impact.
Integer deficit = requiredLevel - employeeLevel;
Integer impact = deficit * importanceMultiplier * weight;
| Factor | Value |
|---|---|
| Required skill | multiplier = 2 |
| Nice‑to‑have | multiplier = 1 |
| Weight | configurable per skill |
Result:
- Missing a critical skill has a higher impact.
- Minor skills don’t disproportionately penalize a candidate.
- The system is realistic and flexible rather than rigid.
Effective Level – Making It Smarter
Raw skill levels aren’t always reliable. I introduced two adjustments to improve accuracy:
- Confidence Adjustment – accounts for who supplied the rating.
- Staleness Adjustment – penalizes outdated assessments.
1. Confidence Adjustment
Boolean isTrusted = (src == 'Manager-assessed');
Integer confidenceAdjust = isTrusted ? 0 : 1;
Integer afterConfidence = rawLevel - confidenceAdjust;
- Self‑assessed → reduce slightly.
- Manager‑assessed → keep unchanged.
2. Staleness Adjustment
Date staleCutoff = Date.today().addMonths(-12);
if (lastVerifiedDate 12 months ago** → slight reduction
---
## Effective Level
```java
Integer effectiveLevel = afterConfidence - stalenessAdjust;
if (effectiveLevel {
public Integer compare(CandidateResult x, CandidateResult y) {
if (x.gapScore != y.gapScore) {
return (x.gapScore **Note:** `Project_Employee_Key__c` is a **Text** field marked **Unique** and **Required**.
Records are saved with an upsert operation:
```java
upsert candidates Project_Employee_Key__c;
The upsert ensures:
- Insert if the record doesn’t exist
- Update the record if it already exists
- Prevent duplicate records
This allows re‑evaluation to update previous scores without creating duplicates.