Enforce Governor-Limit-Safe Batch Processing
By the end of this lesson, you'll be able to:
- Write a Batch Apex class to recalculate balances for all accounts
- Explain why a full recalculation batch job is a useful safety net
- Size the batch scope appropriately for the workload
Prerequisites: "Build Bulk-Safe Transaction Triggers"
Why recalculate at all, if the trigger already keeps it correct
global class RecalculateBalancesBatch implements Database.Batchable<SObject> {
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([SELECT Id, Balance__c FROM Bank_Account__c]);
}
global void execute(Database.BatchableContext bc, List<Bank_Account__c> scope) {
Map<Id, Decimal> sums = new Map<Id, Decimal>();
for (AggregateResult ar : [
SELECT Bank_Account__c acc, SUM(Amount__c) total
FROM Transaction__c
WHERE Bank_Account__c IN :scope AND Type__c = 'Deposit'
GROUP BY Bank_Account__c
]) {
sums.put((Id) ar.get('acc'), (Decimal) ar.get('total'));
}
// (a full solution also subtracts withdrawal sums per account)
for (Bank_Account__c acc : scope) {
acc.Balance__c = sums.containsKey(acc.Id) ? sums.get(acc.Id) : 0;
}
update scope;
}
global void finish(Database.BatchableContext bc) {}
}
global class RecalculateBalancesBatch implements Database.Batchable<SObject> {— the class, implementing the required interface.global Database.QueryLocator start(Database.BatchableContext bc) {— defines what to process.return Database.getQueryLocator([SELECT Id, Balance__c FROM Bank_Account__c]);— every account, potentially thousands.}— closesstart.- (blank line)
global void execute(Database.BatchableContext bc, List<Bank_Account__c> scope) {— runs once per chunk of accounts.Map<Id, Decimal> sums = new Map<Id, Decimal>();— accumulates a fresh sum per account, ignoring any stored value.for (AggregateResult ar : [— Module 20's aggregate SOQL.SELECT Bank_Account__c acc, SUM(Amount__c) total— groups and sums in the query itself.FROM Transaction__c— the underlying object.WHERE Bank_Account__c IN :scope AND Type__c = 'Deposit'— scoped to this chunk's accounts, deposits only.GROUP BY Bank_Account__c— one row per account.]) {— closes the query, opens the loop over its results.sums.put((Id) ar.get('acc'), (Decimal) ar.get('total'));— stores each account's freshly summed total.}— closes theforloop.// (a full solution also subtracts withdrawal sums per account)— a noted simplification.- (blank line)
for (Bank_Account__c acc : scope) {— iterates every account in this chunk.acc.Balance__c = sums.containsKey(acc.Id) ? sums.get(acc.Id) : 0;— overwrites the stored balance with the freshly recalculated one.}— closes theforloop.update scope;— one DML statement per chunk.}— closesexecute.- (blank line)
global void finish(Database.BatchableContext bc) {}— no closing work needed here.}— closes the class.
Even a well-tested trigger (Module 31) can drift from correct over time — a bulk data-load bypassing the trigger, a bug fixed after some records were already affected, or a manual data correction. A periodic recalculation job, using Module 38's exact Database.Batchable shape, is a safety net that self-heals any such drift.
This is Module 38's pattern, not a new one
The start/execute/finish structure, the Database.QueryLocator for scale, and the default 200-record scope are identical to Module 38's Batch Apex lesson — a banking system doesn't need different batch mechanics, it needs the same reliable mechanics applied to a domain where "the numbers must be right" carries more weight than a typical rollup.
Choosing a smaller batch scope deliberately
Database.executeBatch(new RecalculateBalancesBatch(), 50);
Database.executeBatch(new RecalculateBalancesBatch(), 50);— the second argument sets a smaller-than-default chunk size explicitly.
Passing 50 instead of the default 200 reduces the risk of any single batch's aggregate query hitting limits when an account has an unusually large transaction history — a deliberate, documented trade-off (slower overall job, safer per-batch execution) rather than an arbitrary number.
Exercise
As a comment, explain why a bulk data load (e.g. a data migration inserting Transaction__c records directly via the Bulk API with triggers disabled) could cause Balance__c to drift out of sync, and how this batch job fixes that.
Show hint
Think about what the trigger from Lesson 2 actually depends on to run.
Enforce Governor-Limit-Safe Batch Processing 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
A Batch Apex job recalculates every account's balance from scratch against its full transaction history — a periodic correctness safety net, built with the exact Batch Apex mechanics from Module 38.