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.
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.
Capstone Reference Implementation: Data Layer and Apex 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
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.