Build the Service Layer
By the end of this lesson, you'll be able to:
- Write MilestoneStatusService, holding the business logic the domain layer delegates to
- Use dependency injection so the service can be unit tested with a fake selector
- Apply the strategy pattern for a genuinely variable piece of business logic
Prerequisites: "Build the Selector Layer"
Status-transition validation
public with sharing class MilestoneStatusService {
private static final Map<String, Set<String>> ALLOWED_TRANSITIONS = new Map<String, Set<String>>{
'Not Started' => new Set<String>{'In Progress'},
'In Progress' => new Set<String>{'Complete', 'Blocked'},
'Blocked' => new Set<String>{'In Progress'}
};
public static void validateStatusTransition(List<Milestone__c> newMilestones, Map<Id, Milestone__c> oldMap) {
for (Milestone__c m : newMilestones) {
String oldStatus = oldMap.get(m.Id).Status__c;
if (oldStatus != m.Status__c && !ALLOWED_TRANSITIONS.get(oldStatus).contains(m.Status__c)) {
m.addError('Cannot move a Milestone directly from ' + oldStatus + ' to ' + m.Status__c + '.');
}
}
}
}
A Map<String, Set<String>> of allowed transitions is a small, self-contained rules table — readable, testable, and easy to extend without touching the trigger or handler at all.
Project-health recalculation, using dependency injection
public with sharing class ProjectHealthService {
private MilestoneSelector milestoneSelector;
public ProjectHealthService(MilestoneSelector milestoneSelector) {
this.milestoneSelector = milestoneSelector;
}
public ProjectHealthService() {
this(new MilestoneSelector());
}
public Map<Id, String> calculateHealth(Set<Id> projectIds) {
List<Milestone__c> milestones = milestoneSelector.selectByProjectIds(projectIds);
// groups by Project__c, checks for any overdue Milestone, returns 'At Risk' or 'On Track' per project
return new Map<Id, String>(); // full grouping logic omitted for brevity
}
}
Module 43\'s exact dependency-injection shape — a test can call new ProjectHealthService(fakeMilestoneSelector) to supply hand-built Milestone data without any real DML, testing the health-calculation logic in complete isolation.
Where the strategy pattern fits
If different project types eventually need different health rules (a Fixed-Price project might define "at risk" differently than a Time-and-Materials one), an IProjectHealthRule interface — Module 43\'s exact strategy pattern — would let ProjectHealthService accept a rule strategy via its constructor, selected by a factory based on project type, without changing ProjectHealthService itself.
Exercise
As a comment, write a unit test outline (just the setup and assertion, described in comments) for ProjectHealthService.calculateHealth using an injected fake MilestoneSelector, without inserting any real records.
Show hint
Think about what a fake selector would need to return.
Build the Service Layer Quiz
My Notes
Log in to keep private notes on this lesson.
Questions about this lesson
No questions yet — be the first to ask.
Log in to ask a question about this lesson.
Summary
MilestoneStatusService holds this project's core business logic — status-transition validation and project-health recalculation — built with Module 43's dependency injection and strategy patterns from the start, rather than retrofitted later.