Step 6 of 21
·
5 minutes
Don't Charge Twice!
The Scenario
A payment job failed AFTER the payment was processed but BEFORE it was recorded.
When the job retried, it charged the customer again.
This is a classic idempotency problem — retrying a job must not repeat completed steps.
The Challenge
How do we make jobs safe to retry without duplicating irreversible operations like payments?
The Solution
Use
JobContext.runStepOnce("step-name", () -> yourLogic) to mark critical sections.
JobRunr remembers which steps completed, so retries skip already-successful operations.
public void processPayment(JobContext context, Payment payment) {
context.runStepOnce("charge-card", () -> paymentGateway.charge(payment));
context.runStepOnce("send-receipt", () -> emailService.sendReceipt(payment));
context.runStepOnce("update-ledger", () -> ledgerService.record(payment));
}
If the job fails after "charge-card" but before "send-receipt", the retry will skip
the charge and continue from the receipt.
Read the documentation →
Try It Yourself
Make a payment using the credit card you've created earlier and watch the job context track completed steps.
If you manually retry the job, it won't duplicate the payment!
You need to log in to perform write operations. You can still view the code solution and dashboard.