DEV Community

Cover image for Building Skill Align - Part 6 - Project Staffing Assistant(Backend)
Rubasri Srikanthan
Rubasri Srikanthan

Posted on

Building Skill Align - Part 6 - Project Staffing Assistant(Backend)

I started with the first feature in this project: Project Staffing Assistant.

Project Staffing Assistant helps managers decide which candidates are suitable for a project based on actual project requirements.

I began with the backend, building the intelligence layer in Apex.


The Core Service – SkillEvaluatorService

public with sharing class SkillEvaluatorService 
Enter fullscreen mode Exit fullscreen mode

Two important design decisions:

  • public → Required because LWC will call this Apex class

  • with sharing → Ensures record-level security is respected

I had previously configured roles, OWD, and sharing rules (Refer here).
Using with sharing ensures 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 sharing enforces record-level sharing rules only, ensuring queries and DML respect the current user’s access.

  • with sharing does not enforce object or field permissions. You must explicitly handle CRUD/FLS (e.g., WITH SECURITY_ENFORCED or Security.stripInaccessible()).

  • If no sharing keyword is defined, the class inherits sharing from its caller, so behavior may vary depending on depending on how it is invoked.

  • Triggers run in system context. Even if a helper class is marked with sharing, the trigger executes in system mode.


Designing Data Transfer Objects

Instead of returning raw Employee__c or Employee_Skill__c records, I created Data Transfer Objects or DTOs.

DTOs define the structured connection between backend and UI. They wrap only the fields required by the frontend, preventing unnecessary exposure of internal data.

For this feature, the UI needed:

  1. Detailed skill gap information (for manager-level decision making)

  2. Candidate-level summary information

Note: @AuraEnabled is required for LWC(UI) to access Apex properties and methods.


Skill-Level DTO

public class SkillGapDetail {
    @AuraEnabled public String skillName;
    @AuraEnabled public Integer requiredLevel;
    @AuraEnabled public Integer impact;
}
Enter fullscreen mode Exit fullscreen mode

Represents a single skill gap for a candidate.

Advantages:

  • All evaluation logic runs in Apex, so 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;
}
Enter fullscreen mode Exit fullscreen mode

For each evaluated employee, the UI receives:

  • Employee name

  • Final gap score

  • Ready / Not Ready flag

  • Skill Gap Detail

This keeps the response clean and structured.


Entry Point – evaluateProject()

@AuraEnabled
public static List<CandidateResult> evaluateProject(Id projectId, Integer topN)
Enter fullscreen mode Exit fullscreen mode

Responsibilities:

  • Accept a Project Id

  • Evaluate unallocated employees

  • Rank them

  • Return top N candidates

  • Persist evaluation results


Guard Clause

  • Guard clauses help prevent unnecessary processing and avoid unexpected or confusing UI behavior.
if (projectId == null) return new List<CandidateResult>();
Enter fullscreen mode Exit fullscreen mode

If no project is provided, evaluation stops.

Prevents:

  • Null pointer exceptions

  • Unexpected UI errors

  • Wasted governor limits


Load Project Requirements

List<Project_Skill_Requirement__c> reqs = [
    SELECT Skill__c, Required_Level__c,
           Importance__c, Weight__c
    FROM Project_Skill_Requirement__c
    WHERE Project__c = :projectId
];
Enter fullscreen mode Exit fullscreen mode

Each requirement contains:

  • Skill

  • Required Level

  • Importance (Required / Nice-to-have)

  • Weight

After fetching, I converted them into Maps for fast access.

Why Maps?

Governor limits restrict queries per transaction. Querying inside loops risks hitting limits. By storing data in Maps:

  • Avoid repeated SOQL calls

  • Ensure constant-time lookups (O(1))

  • Keep code bulk-safe

Maps are essential in Apex for this reason.


Weighted Impact Formula

This is the heart of the evaluation engine.

I first compute the deficit to rank candidates:
deficit = requiredLevel - employeeLevel;

By itself, this treats all skills equally. To make evaluations more realistic, I introduced weighted scoring:

Integer impact = deficit * importanceMultiplier * weight;
Enter fullscreen mode Exit fullscreen mode

Where:

  • Required skill → multiplier = 2

  • Nice-to-have → multiplier = 1

  • Weight → configurable per skill

From this I ensured,

  • Missing a critical skill has higher impact

  • Minor skills don’t disproportionately penalize a candidate

The result is a system that is realistic and flexible rather than rigid.


Effective Level – Making It Smarter

Raw skill levels aren’t always reliable. To improve accuracy, I introduced two adjustments:

  1. Confidence adjustment
  2. Staleness adjustment

1. Confidence Adjustment

Boolean isTrusted = (src == 'Manager-assessed');
Integer confidenceAdjust = isTrusted ? 0 : 1;
Integer afterConfidence = rawLevel - confidenceAdjust;
Enter fullscreen mode Exit fullscreen mode
  • Self-assessed → reduce slightly
  • Manager-assessed → keep unchanged

2. Staleness Adjustment

Date staleCutoff = Date.today().addMonths(-12);

if (lastVerified == null) {
    stalenessAdjust = 2;
} else if (lastVerified <= staleCutoff) {
    stalenessAdjust = 1;
}
Enter fullscreen mode Exit fullscreen mode

Never verified → larger reduction

Verified >12 months ago → slight reduction
Finally, the effective level is computed as:

Integer effectiveLevel = afterConfidence - stalenessAdjust;
if (effectiveLevel < 0) effectiveLevel = 0;
Enter fullscreen mode Exit fullscreen mode

This makes the evaluation time and credibility aware, preventing outdated or inflated skill ratings from misleading staffing decisions.


Ranking Candidates

results.sort(new CandidateComparator());
Enter fullscreen mode Exit fullscreen mode

Custom comparator:

private class CandidateComparator implements System.Comparator<CandidateResult> {
    public Integer compare(CandidateResult x, CandidateResult y) {
        if (x.gapScore != y.gapScore) {
            return (x.gapScore < y.gapScore) ? -1 : 1;
        }
        return x.employeeName.toLowerCase()
               .compareTo(y.employeeName.toLowerCase());
    }
}
Enter fullscreen mode Exit fullscreen mode

Sorting priority :

  1. Lowest gap score

  2. Alphabetical order as tie-breaker

Using this comparator ensures deterministic sorting, providing consistent results across repeated evaluations.


Project Ready Logic

cr.isProjectReady = (requiredImpact == 0);
Enter fullscreen mode Exit fullscreen mode

If all required skills have zero impact, the candidate is ready.

Nice-to-have gaps don’t block readiness, preventing unnecessary hiring when existing employees are suitable.


Persisting Recommendations

The evaluation results are stored in the Project_Candidate__c object.

A composite key is used to uniquely identify each candidate for a project:

pc.Project_Employee_Key__c =
    String.valueOf(projectId) + '|' + String.valueOf(employeeId);
Enter fullscreen mode Exit fullscreen mode

Note: - The Project_Employee_Key__c is a Text field marked Unique and Required.

The records are then saved using:

upsert candidates Project_Employee_Key__c;
Enter fullscreen mode Exit fullscreen mode

upsert ensures:

  • Insert if record doesn’t exist

  • Updates the record if it already exists

  • Prevents duplicate records

  • Allows re-evaluation to update previous scores


Top comments (2)

Collapse
 
harsh2644 profile image
Harsh

Great to see the progress on Skill Align! Project Staffing Assistant sounds like a really useful feature excited to see how you've implemented it. Salesforce devs like you inspire me to start my own side project!

Collapse
 
rubasri_srikanthan profile image
Rubasri Srikanthan

Thank you so much! I’m really happy the Project Staffing Assistant caught your attention

Building backend logic in Salesforce has been challenging but super rewarding.

You should absolutely start your own side project — that’s where real growth happens.