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 + '.');
            }
        }
    }
}
  1. public with sharing class MilestoneStatusService { — the service class.
  2. private static final Map<String, Set<String>> ALLOWED_TRANSITIONS = new Map<String, Set<String>>{ — a named constant, not scattered if conditions.
  3. 'Not Started' => new Set<String>{'In Progress'}, — one entry per starting status.
  4. 'In Progress' => new Set<String>{'Complete', 'Blocked'}, — allowed next statuses.
  5. 'Blocked' => new Set<String>{'In Progress'} — allowed next statuses.
  6. }; — closes the map literal.
  7. (blank line)
  8. public static void validateStatusTransition(List<Milestone__c> newMilestones, Map<Id, Milestone__c> oldMap) { — plain parameters, no trigger dependency.
  9. for (Milestone__c m : newMilestones) { — iterates every updated Milestone.
  10. String oldStatus = oldMap.get(m.Id).Status__c; — looks up the prior status.
  11. if (oldStatus != m.Status__c && !ALLOWED_TRANSITIONS.get(oldStatus).contains(m.Status__c)) { — only checks transitions that actually changed, against the rules table.
  12. m.addError('Cannot move a Milestone directly from ' + oldStatus + ' to ' + m.Status__c + '.'); — blocks the record with a specific message.
  13. } — closes the if.
  14. } — closes the for loop.
  15. } — closes the method.
  16. } — closes the class.

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
    }
}
  1. public with sharing class ProjectHealthService { — the service class.
  2. private MilestoneSelector milestoneSelector; — a field, not a local variable — the injected dependency.
  3. (blank line)
  4. public ProjectHealthService(MilestoneSelector milestoneSelector) { — a constructor that accepts the dependency.
  5. this.milestoneSelector = milestoneSelector; — stores whatever was passed in.
  6. } — closes that constructor.
  7. (blank line)
  8. public ProjectHealthService() { — a second, no-argument constructor.
  9. this(new MilestoneSelector()); — delegates to the first constructor with a real selector.
  10. } — closes the no-argument constructor.
  11. (blank line)
  12. public Map<Id, String> calculateHealth(Set<Id> projectIds) { — the method signature.
  13. List<Milestone__c> milestones = milestoneSelector.selectByProjectIds(projectIds); — uses this.milestoneSelector, whichever one was injected.
  14. // groups by Project__c, checks for any overdue Milestone, returns 'At Risk' or 'On Track' per project — the actual logic, summarized.
  15. return new Map<Id, String>(); // full grouping logic omitted for brevity — a placeholder return.
  16. } — closes the method.
  17. } — closes the class.

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.