Handle Declines and Retries
By the end of this lesson, you'll be able to:
- Distinguish a decline (no retry) from an error (retry) in the Queueable job
- Implement a simple retry with a maximum attempt count
- Explain why retrying a decline would be a business logic mistake
Prerequisites: "Make It Asynchronous (Queueable)"
Why a decline should never be retried
Retrying a definitively declined charge — the same card, the same amount, immediately again — accomplishes nothing except annoying the customer and potentially triggering fraud detection on the gateway's side. This is exactly why Lesson 1 defined declined as a distinct, non-retryable outcome from error in the first place.
Retrying an error, with a limit
public class ProcessPaymentJob implements Queueable {
private Id orderId;
private Decimal amount;
private String paymentToken;
private Integer attemptNumber;
public ProcessPaymentJob(Id orderId, Decimal amount, String paymentToken, Integer attemptNumber) {
this.orderId = orderId;
this.amount = amount;
this.paymentToken = paymentToken;
this.attemptNumber = attemptNumber;
}
public void execute(QueueableContext context) {
PaymentResult result = new PaymentGatewayService().charge(amount, paymentToken);
Order__c order = new Order__c(Id = orderId);
if (result.success) {
order.Status__c = 'Paid';
order.Payment_Reference__c = result.gatewayTransactionId;
update order;
} else if (result.declined) {
order.Status__c = 'Payment Declined';
order.Decline_Reason__c = result.message;
update order;
} else if (attemptNumber < 3) {
System.enqueueJob(new ProcessPaymentJob(orderId, amount, paymentToken, attemptNumber + 1));
} else {
order.Status__c = 'Payment Error';
update order;
}
}
}
attemptNumber (Module 9's parameter passing, extended with one more field) tracks how many times this specific charge has been attempted — an error triggers Module 38's job-chaining pattern to retry, but only up to 3 attempts, after which it gives up and records the final error status.
Updating the trigger handler for the new constructor
if (justReadyForPayment) {
System.enqueueJob(new ProcessPaymentJob(order.Id, order.Total__c, order.Payment_Token__c, 1));
}
The trigger's original enqueue call now passes 1 as the starting attemptNumber — the first attempt — with ProcessPaymentJob itself handling every retry afterward via chaining, never requiring the original trigger to know or care about retries at all.
Exercise
As a comment, explain what would go wrong if the else if (attemptNumber < 3) branch also ran for a decline, not just an error.
Show hint
Think about what retrying an already-declined charge would actually mean for a customer.
Handle Declines and Retries 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
This lesson implements Lesson 1's success/decline/error distinction fully — specifically, automatically retrying an error (but never a decline), with a maximum attempt count to avoid retrying forever.