Here’s a complete, production-shaped Batch Apex class — the kind interviewers want to see (not the textbook stub).
The class
/**
* Auto-closes Cases that have been New for more than 30 days.
* Tracks totals across chunks (Database.Stateful) and emails a
* summary at the end.
*/
public class StaleCaseCloserBatch implements
Database.Batchable<sObject>,
Database.Stateful
{
private Integer totalProcessed = 0;
private Integer totalErrors = 0;
private List<String> errorMessages = new List<String>();
public Database.QueryLocator start(Database.BatchableContext ctx) {
return Database.getQueryLocator(
'SELECT Id, Subject, Status, AccountId, OwnerId ' +
'FROM Case ' +
'WHERE Status = \'New\' ' +
' AND CreatedDate < LAST_N_DAYS:30'
);
}
public void execute(Database.BatchableContext ctx, List<Case> scope) {
for (Case c : scope) {
c.Status = 'Auto-Closed';
c.Description = (c.Description != null ? c.Description + '\n' : '') +
'[' + Datetime.now() + '] Auto-closed by stale-case batch.';
}
Database.SaveResult[] results = Database.update(scope, false);
for (Integer i = 0; i < results.size(); i++) {
if (results[i].isSuccess()) {
totalProcessed++;
} else {
totalErrors++;
errorMessages.add(
'Case ' + scope[i].Id + ': ' +
results[i].getErrors()[0].getMessage()
);
}
}
}
public void finish(Database.BatchableContext ctx) {
AsyncApexJob job = [
SELECT Status, JobItemsProcessed, TotalJobItems, CreatedBy.Email
FROM AsyncApexJob WHERE Id = :ctx.getJobId()
];
Messaging.SingleEmailMessage msg = new Messaging.SingleEmailMessage();
msg.setToAddresses(new String[] { job.CreatedBy.Email });
msg.setSubject('Stale Case Closer: ' + job.Status);
msg.setPlainTextBody(
'Chunks: ' + job.JobItemsProcessed + ' of ' + job.TotalJobItems + '\n' +
'Records updated: ' + totalProcessed + '\n' +
'Errors: ' + totalErrors + '\n\n' +
(totalErrors > 0
? 'First 10 errors:\n' + String.join(safeFirst(errorMessages, 10), '\n')
: 'No errors.')
);
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { msg });
}
private List<String> safeFirst(List<String> list, Integer n) {
if (list.size() <= n) return list;
List<String> top = new List<String>();
for (Integer i = 0; i < n; i++) top.add(list[i]);
return top;
}
}
How to invoke
Id jobId = Database.executeBatch(new StaleCaseCloserBatch(), 200);
What this example demonstrates
| Feature | Shown by |
|---|---|
Database.Batchable<sObject> | Class declaration |
Database.Stateful | Member variables persist across execute |
Database.QueryLocator | Used in start for up to 50M records |
| Partial-success DML | Database.update(scope, false) — failed records don’t fail the chunk |
| Per-record error tracking | Iterating SaveResult[] |
AsyncApexJob query in finish | Reports job status |
| Notification via email | Standard pattern |
A simpler version
If they just want the bare minimum:
public class SimpleBatch implements Database.Batchable<sObject> {
public Database.QueryLocator start(Database.BatchableContext ctx) {
return Database.getQueryLocator('SELECT Id FROM Account WHERE Active__c = false');
}
public void execute(Database.BatchableContext ctx, List<Account> scope) {
for (Account a : scope) a.Active__c = true;
update scope;
}
public void finish(Database.BatchableContext ctx) {}
}
The version with callouts
If execute makes HTTP callouts, add Database.AllowsCallouts:
public class SyncBatch implements
Database.Batchable<sObject>,
Database.AllowsCallouts
{
public Database.QueryLocator start(Database.BatchableContext ctx) { /* ... */ }
public void execute(Database.BatchableContext ctx, List<Account> scope) {
for (Account a : scope) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:ERP/accounts/' + a.Id);
req.setMethod('PUT');
new Http().send(req);
}
}
public void finish(Database.BatchableContext ctx) {}
}
Verified against: Apex Developer Guide — Using Batch Apex. Last reviewed 2026-05-17.