Advanced 40 min read

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.

APEX

Build the Service Layer Quiz

1. Why is ProjectHealthService built with a constructor that accepts an injected MilestoneSelector, following Module 43's dependency injection pattern?

Log in to submit the quiz and save your score.

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.