Advanced 35 min read

Build the Service Layer

By the end of this lesson, you'll be able to:

  • Implement each health metric as its own focused method
  • Combine them into a single wrapper class for the dashboard
  • Keep the service layer entirely free of UI concerns

Prerequisites: "Design the Data Aggregation"

Each metric as its own method

public class AccountHealthService {
    public Integer getOpenCaseCount(Id accountId) {
        return [SELECT COUNT() FROM Case WHERE AccountId = :accountId AND IsClosed = false];
    }

    public Decimal getAverageResolutionDays(Id accountId) {
        List<Case> closedCases = [
            SELECT CreatedDate, ClosedDate FROM Case
            WHERE AccountId = :accountId AND IsClosed = true AND ClosedDate != null
        ];

        if (closedCases.isEmpty()) {
            return 0;
        }

        Integer totalDays = 0;
        for (Case c : closedCases) {
            totalDays += c.CreatedDate.date().daysBetween(c.ClosedDate.date());
        }
        return totalDays / (Decimal) closedCases.size();
    }
}
  1. public class AccountHealthService { — the class.
  2. public Integer getOpenCaseCount(Id accountId) { — the first metric method.
  3. return [SELECT COUNT() FROM Case WHERE AccountId = :accountId AND IsClosed = false]; — Module 20's COUNT() aggregate function, filtered to this Account's still-open Cases.
  4. } — closes getOpenCaseCount.
  5. (blank line)
  6. public Decimal getAverageResolutionDays(Id accountId) { — the second metric method.
  7. List<Case> closedCases = [ — opens a SOQL query, since averaging a date difference needs the actual records, not just a count.
  8. SELECT CreatedDate, ClosedDate FROM Case — only the two fields the average needs.
  9. WHERE AccountId = :accountId AND IsClosed = true AND ClosedDate != null — closed Cases for this Account that actually have a resolution date.
  10. ]; — closes the query, assigning the results to closedCases.
  11. (blank line)
  12. if (closedCases.isEmpty()) { — Module 17's "Handle Edge Cases" habit, checked up front.
  13. return 0; — an Account with no closed Cases yet has no average to compute — 0 rather than a divide-by-zero error.
  14. } — closes the if.
  15. (blank line)
  16. Integer totalDays = 0; — an accumulator, initialized before the loop.
  17. for (Case c : closedCases) { — iterates the already-queried list — no SOQL inside this loop.
  18. totalDays += c.CreatedDate.date().daysBetween(c.ClosedDate.date()); — Apex's Date.daysBetween gives the whole-day gap between the two dates, added to the running total.
  19. } — closes the for loop.
  20. return totalDays / (Decimal) closedCases.size(); — casting to Decimal first ensures true division rather than truncating integer division.
  21. } — closes getAverageResolutionDays.
  22. } — closes the class.

getOpenCaseCount uses Module 20's COUNT() directly; getAverageResolutionDays computes the average in Apex since it needs a date difference SOQL's AVG() alone can't express — both handle the empty-data case (Module 17's "Handle Edge Cases" habit) up front.

Combining metrics into one wrapper

public class AccountHealthSummary {
    public Integer openCaseCount;
    public Decimal averageResolutionDays;
    public Decimal totalWonRevenue;
    public Integer daysSinceLastActivity;
}

public AccountHealthSummary getHealthSummary(Id accountId) {
    AccountHealthSummary summary = new AccountHealthSummary();
    summary.openCaseCount = getOpenCaseCount(accountId);
    summary.averageResolutionDays = getAverageResolutionDays(accountId);
    summary.totalWonRevenue = getTotalWonRevenue(accountId);
    summary.daysSinceLastActivity = getDaysSinceLastActivity(accountId);
    return summary;
}
  1. public class AccountHealthSummary { — a plain wrapper class, no annotations at all yet.
  2. public Integer openCaseCount; — one field per metric.
  3. public Decimal averageResolutionDays; — same.
  4. public Decimal totalWonRevenue; — same.
  5. public Integer daysSinceLastActivity; — same.
  6. } — closes the wrapper class.
  7. (blank line)
  8. public AccountHealthSummary getHealthSummary(Id accountId) { — the combining method.
  9. AccountHealthSummary summary = new AccountHealthSummary(); — one instance to populate.
  10. summary.openCaseCount = getOpenCaseCount(accountId); — delegates to the focused method already written.
  11. summary.averageResolutionDays = getAverageResolutionDays(accountId); — same.
  12. summary.totalWonRevenue = getTotalWonRevenue(accountId); — same (implemented in this lesson's exercise).
  13. summary.daysSinceLastActivity = getDaysSinceLastActivity(accountId); — same.
  14. return summary; — one populated object, ready for a caller.
  15. } — closes getHealthSummary.

This is Module 34's DTO/wrapper-class pattern, used here at the service layer rather than directly in the controller — AccountHealthSummary doesn't yet have any @AuraEnabled annotations, since this class has no idea an LWC will ever use it (that's the controller's job, in a later lesson).

Why the service layer stays UI-agnostic

AccountHealthService and AccountHealthSummary could be called from an LWC controller, a batch job, or a plain Execute Anonymous script equally easily — nothing here mentions @AuraEnabled, cacheable, or anything LWC-specific. This mirrors Module 26's AccountRollupService: the logic that computes something and the logic that exposes it to a UI are deliberately kept as separate concerns.

Exercise

Add a getTotalWonRevenue(Id accountId) method that queries Total_Won_Revenue__c directly from the Account.

Show hint

[SELECT Total_Won_Revenue__c FROM Account WHERE Id = :accountId].Total_Won_Revenue__c

Salesforce: ○ Not connected Connect
APEX

Execution Results

Run your code to see the result.

Build the Service Layer Quiz

1. Why does AccountHealthService have no @AuraEnabled annotations anywhere in it?

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

This lesson implements AccountHealthService — plain Apex, with no knowledge of LWC or @AuraEnabled at all, exactly the separation Module 34's controller lesson will build on top of.