A minimal Queueable is six lines, but interviewers want to see you cover the patterns that come up in real projects: typed constructor input, optional callouts, and a test class with Test.startTest() and Test.stopTest() around the enqueue.
Production-shaped Queueable
public with sharing class AccountSyncQueueable implements Queueable {
private final List<Account> accountsToSync;
public AccountSyncQueueable(List<Account> accountsToSync) {
this.accountsToSync = accountsToSync;
}
public void execute(QueueableContext qc) {
// Do some work — for example, stamp a sync timestamp
for (Account a : accountsToSync) {
a.Last_Synced__c = System.now();
}
update accountsToSync;
// Chain the next job if there's more to do
List<Account> remaining = [
SELECT Id
FROM Account
WHERE Last_Synced__c = null
LIMIT 100
];
if (!remaining.isEmpty() && !Test.isRunningTest()) {
System.enqueueJob(new AccountSyncQueueable(remaining));
}
}
}
What the interviewer is looking for in this snippet:
- Typed constructor —
List<Account>(not a list of Ids you re-query insideexecute()). finalfield — clear that the input is set at construction and not mutated by the framework.- Chaining with
System.enqueueJobto continue the work when more records remain. - Test guard on chaining so tests don’t recurse.
Submitting the job
List<Account> firstBatch = [
SELECT Id, Last_Synced__c
FROM Account
WHERE Last_Synced__c = null
LIMIT 100
];
Id jobId = System.enqueueJob(new AccountSyncQueueable(firstBatch));
System.debug('Enqueued job: ' + jobId);
System.enqueueJob returns the AsyncApexJob.Id — store it if you need to poll status from another transaction.
Queueable with callouts
Add the Database.AllowsCallouts marker:
public class WebhookQueueable implements Queueable, Database.AllowsCallouts {
private final Id recordId;
public WebhookQueueable(Id recordId) { this.recordId = recordId; }
public void execute(QueueableContext qc) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Webhook/notify');
req.setMethod('POST');
req.setBody(JSON.serialize(new Map<String, Id>{ 'id' => recordId }));
HttpResponse res = new Http().send(req);
System.debug(res.getStatus());
}
}
Test class
@isTest
private class AccountSyncQueueableTest {
@isTest
static void syncsAccounts() {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 5; i++) {
accounts.add(new Account(Name = 'Test ' + i));
}
insert accounts;
Test.startTest();
System.enqueueJob(new AccountSyncQueueable(accounts));
Test.stopTest();
for (Account a : [SELECT Last_Synced__c FROM Account]) {
System.assertNotEquals(null, a.Last_Synced__c, 'Should have been synced');
}
}
}
Test.stopTest() forces the queued job to run synchronously, so the asserts after it inspect the real result.
Common follow-ups
- Why not
@future? — Typed inputs, chaining, and a real job Id. See the Queueable interface question for the full comparison. - Chain limit? — 5 levels deep in production, unlimited in tests.
- What’s
QueueableContextfor? —qc.getJobId()gives you the currentAsyncApexJobId (useful for logging).
Verified against: Apex Developer Guide — Queueable Apex. Last reviewed 2026-05-17 for Spring ‘26.