
Error Handling
Part of Idempotency and duplicate prevention
Preventing duplicate invoices after a workflow retry
Protect invoice creation across workflow retries, lost responses and concurrent workers using a stable business identity.
To avoid duplicate invoices, bind invoice creation to a stable business identity, such as the approved order and invoice purpose. On retry, reuse that identity and find the invoice already created, even if the first workflow attempt lost its response. Treat the invoice system as part of the safeguard: a local “processed” flag alone can miss a failure between systems.
Trace the uncertain outcome
Suppose a workflow sends a create-invoice request, gets no response and retries. The first request may have failed, or it may have succeeded and its response was lost.
If the second attempt creates a fresh invoice, the workflow has turned uncertainty into a duplicate. The same problem arises when two workers pick up the same event. Azure’s messaging guidance describes repeated delivery and recommends idempotent consumer processing.
List every place an invoice can be created: the workflow, a human retry button, another integration and the billing API itself. If any path bypasses the identity check, the protection is incomplete. Decide whether an amended invoice, recurring billing period or separate purchase is a new action; those must remain possible.
Key Metrics for Invoice Idempotency
- Idempotency Key Retention (Stripe)
- 24 hours minimum
- Risk of Duplicate on Retry Without Idempotency
- High – can lead to double billing
- Azure Messaging Guidance on Repeated Delivery
- Recommend idempotent consumer processing
Use a stable invoice identity
A practical design might record tenant + order ID + invoice type as a unique invoice request. The exact fields depend on the billing rules. Save the request identity, its status and the resulting invoice ID. When the same request arrives again, return or look up that invoice instead of issuing another create call.
If the billing API supports an idempotency key, pass the same key on each retry of that call and follow its documented retention and conflict rules. Stripe’s published API contract is one example: repeated matching requests with the same key return the recorded result, but keys can be pruned after a minimum retention period. Do not assume this protects a second billing provider or an invoice created manually.
Two concurrent attempts also need handling. A “check then create” lookup without a unique constraint or transactional claim can let both workers proceed. Make the identity unique in a durable store, and define what the losing worker reads or returns. If the first worker is still running, distinguish “pending” from “complete” rather than marking the work done too early.
Reconcile after a partial failure
If the external invoice exists but the local workflow never recorded its ID, search the billing system by the stable reference or retry with the same supported key. Create a new invoice only when the earlier outcome is resolved. For a system without a reliable lookup or idempotency contract, route uncertain cases for review instead of blindly repeating the create step.
Test the exact failure window: create an invoice, interrupt the workflow before it records success, then retry. Confirm the billing system contains one invoice and the workflow points to it. Repeat with simultaneous workers and with a legitimate second invoice type. This verifies the business result, not merely that the retry step displayed “success”.
![How to choose the right idempotency key for an event: Keep the same idempotency key for retries of one action; use a new key for a new action.; Write: one [effect] for each [identity], e.g. one standard invoice per approved order.; Check stability, uniqueness, scope and lifetime; include tenant ID for tenant-scoped IDs. Choosing an idempotency key for an event](/covers/choosing-an-idempotency-key-for-an-event-640.webp?v=39704930)


