Advanced 30 min read

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.

APEX

Handle Declines and Retries Quiz

1. What does attemptNumber protect against?

Log in to submit the quiz and save your score.

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.