Advanced 40 min read

Capstone Reference Implementation: Data Layer and Apex

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

  • Implement the Selector/Service/Controller layers planned in the previous lesson
  • Apply AccessLevel.USER_MODE and AuraHandledException consistently
  • Build wrapper DTOs shaping exactly what the component needs

Prerequisites: "Capstone Requirements and Architecture Planning"

The Selector Layer

public inherited sharing class CaseSelector {
    public static List<Case> openCases() {
        return Database.query(
            'SELECT Id, Subject, Status, AccountId FROM Case WHERE IsClosed = false ORDER BY CreatedDate DESC',
            AccessLevel.USER_MODE
        );
    }
}

public inherited sharing class AccountSelector {
    public static Account summaryById(Id accountId) {
        List<Account> results = Database.query(
            'SELECT Id, Name, Industry, (SELECT Id, Amount FROM Opportunities WHERE IsClosed = false) ' +
            'FROM Account WHERE Id = :accountId',
            AccessLevel.USER_MODE
        );
        return results.isEmpty() ? null : results[0];
    }
}

Each Selector owns exactly one query concern, isolated so it can change independently of the business logic that consumes it (Module 16).

The Service Layer

public inherited sharing class CommandCenterService {
    public static List<CaseSummary> getOpenCases() {
        List<CaseSummary> summaries = new List<CaseSummary>();
        for (Case c : CaseSelector.openCases()) {
            summaries.add(new CaseSummary(c.Id, c.Subject, c.Status, c.AccountId));
        }
        return summaries;
    }

    public static AccountSummary getAccountSummary(Id accountId) {
        Account acc = AccountSelector.summaryById(accountId);
        if (acc == null) {
            throw new AuraHandledException('Account not found.');
        }

        Decimal openPipeline = 0;
        for (Opportunity opp : acc.Opportunities) {
            openPipeline += opp.Amount;
        }
        return new AccountSummary(acc.Id, acc.Name, acc.Industry, openPipeline);
    }

    public static void updateCaseStatus(Id caseId, String newStatus) {
        if (String.isBlank(newStatus)) {
            throw new AuraHandledException('A status is required.');
        }
        Database.update(new Case(Id = caseId, Status = newStatus), AccessLevel.USER_MODE);
    }
}

No @AuraEnabled annotations here at all (Module 16) — this class doesn't know or care that LWC exists, keeping it independently testable and reusable. AuraHandledException (Module 7) surfaces safe, specific validation messages.

The Controller Layer

public with sharing class CommandCenterController {
    @AuraEnabled(cacheable=true)
    public static List<CaseSummary> getOpenCases() {
        return CommandCenterService.getOpenCases();
    }

    @AuraEnabled(cacheable=true)
    public static AccountSummary getAccountSummary(Id accountId) {
        return CommandCenterService.getAccountSummary(accountId);
    }

    @AuraEnabled
    public static void updateCaseStatus(Id caseId, String newStatus) {
        CommandCenterService.updateCaseStatus(caseId, newStatus);
    }
}

Every method here is a one-line pass-through — genuinely thin, exactly as planned in Lesson 1. getOpenCases/getAccountSummary are cacheable=true for @wire (read-only); updateCaseStatus isn't, since it performs DML and is called imperatively (Module 7).

Exercise

Explain, as a comment, why CommandCenterService.updateCaseStatus is not marked cacheable=true.

Show hint

Recall what cacheable=true requires of a method.

APEX

Exercise

Challenge: explain, as a comment, why CommandCenterService has zero @AuraEnabled annotations despite being central to this feature.

Show hint

Recall Module 16's Service layer design.

APEX

Capstone Reference Implementation: Data Layer and Apex Quiz

1. What does CaseSelector.openCases() own exclusively?

2. What throws when getAccountSummary is called with an Id that doesn't match any Account?

3. What enforces CRUD/FLS/sharing on the Selector queries?

4. Which layer contains the @AuraEnabled annotations?

5. Why is updateCaseStatus called imperatively rather than via @wire?

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

The planned architecture becomes real code — Selectors isolating queries, a Service holding the actual logic, and a thin Controller exposing exactly what the component needs.