Idempotency Pattern Guide for QRIS Webhooks: Database Schema, Retry Logic, and Laravel Implementation

162% QRIS Growth: Why Idempotency Pattern Is Urgent
QRIS recorded transaction growth of 162% by July 2025. This figure shows massive adoption of digital payments in Indonesia. Yet behind this growth number, the classic webhook payment gateway problem remains the same: notifications can be lost due to server down or connection failure, and transactions can duplicate because webhooks are sent repeatedly.
Idempotency ensures one payment intent produces only one financial outcome, even if requests are sent repeatedly. This pattern becomes the standard solution to prevent double charge that can harm businesses and customers.
Idempotency Architecture for QRIS Webhooks
The standard solution for webhooks involves three main pillars: idempotency key + unique reference ID, server-side deduplication, and retry mechanism with exponential backoff. Proper implementation requires a database schema that supports tracking transaction status and prevents duplicate processing.
Minimal database schema for idempotency includes: transaction tables with unique columns for external_id (reference ID from payment gateway), idempotency_key column, status column, and processed_at column. Indexes on external_id and idempotency_key ensure fast queries during validation.
Indonesia's digital economy value is projected to exceed 100 billion US dollars in 2025. At this scale, businesses cannot rely on manual reconciliation to handle duplicate transactions. Automation through idempotency pattern is no longer nice-to-have, but an operational necessity.
Database Schema Implementation for Idempotency
For Laravel implementation, database schema can start with migration that creates transactions and webhook_logs tables. The transactions table stores the final status of each transaction, while webhook_logs records every webhook receipt for audit trail.
Basic schema example:
- transactions table: id, external_id (unique), idempotency_key (unique), amount, status (pending/success/failed), processed_at, created_at, updated_at
- webhook_logs table: id, transaction_id, payload, received_at, processed_at, processing_duration_ms
Unique constraint on external_id and idempotency_key is the first line of defense. Database will reject duplicate inserts before reaching application layer. For retry logic, add retry_count and last_retry_at columns to the webhook_logs table.
Retry Logic with Exponential Backoff in Laravel
Retry mechanism must be designed to handle webhooks that fail temporarily (for example, third-party API is down). Exponential backoff ensures the system doesn't flood the server with consecutive retry requests.
In Laravel, retry logic implementation can use Job Queue with built-in retry mechanism. Each incoming webhook dispatches a job to queue. If the job fails, Laravel will retry with increasing delay (for example: 60 seconds, 5 minutes, 30 minutes).
It's important to separate between webhook receive endpoint and webhook processing. Receive endpoint only records logs and provides 200 OK response as quickly as possible. Processing is done asynchronously by background jobs. This prevents timeout on payment gateways sending webhooks.
Laravel Code Example: Idempotency Pattern
Practical implementation starts with a controller that receives webhooks. This controller doesn't process transactions directly, but stores payload and validates idempotency.
Basic structure example:
- WebhookController: receive request, validate signature, save to webhook_logs, dispatch job
- ProcessWebhookJob: validate idempotency, process business logic, update transaction status
- Service layer: encapsulate business logic separate from infrastructural concern
Idempotency check is done by finding existing records based on idempotency_key or external_id. If record is found with success status, skip processing. If record is found with pending/failed status, continue processing with idempotency check on operations that perform state changes (for example: update stock, send email).
For operations that change external state (calling third-party APIs), consider making side-effects idempotent by recording IDs from external responses and validating before re-execution.
Best Practices and Gotchas
Although idempotency pattern is relatively straightforward, there are several pitfalls to avoid. First, don't rely on application-layer check alone. Database constraint is needed as the last line of defense against race conditions.
Second, be careful with partial processing. If one transaction involves several operations (update order, send email, update loyalty), ensure all operations are idempotent or use saga pattern for compensation if middle steps fail.
Third, logging and monitoring are crucial. Without visibility into retry attempts and failure patterns, problems can be detected too late. Monitor metrics like webhook receive rate, processing lag, retry count distribution, and failure rate per payment gateway.
Overall, idempotency pattern is the right investment for businesses relying on QRIS or other payment gateways. With 162% QRIS transaction growth and Indonesia's digital economy projected to exceed 100 billion US dollars in 2025, scalable and reliable systems become a competitive advantage.
Implementing idempotency doesn't have to be complex. Start with proper database schema, simple retry logic, and code structure that separates concerns. For Indonesian businesses wanting to implement QRIS or other payment gateways with correct idempotency pattern, Colabs can help from architecture design to production-ready implementation.

