Blog
Backend9 min read
Designing Idempotent Payment Webhooks in Spring Boot
B
BADJO Dibéa Koffi
Published on May 17, 2026
The Webhook Problem
Payment providers retry webhooks until they get a 200. Stripe retries up to 15 times over 3 days. A retry means double-charging, double-decrementing inventory, and duplicate emails.
Rule 1: Verify the Signature
@PostMapping("/webhooks/stripe")
public ResponseEntity<Void> handleStripe(
@RequestBody String payload,
@RequestHeader("Stripe-Signature") String signature) {
Event event = Webhook.constructEvent(payload, signature, webhookSecret);
processEvent(event);
return ResponseEntity.ok().build();
}Rule 2: Return 200 Before Processing
webhookEventRepo.save(new WebhookEvent(event.getId(), "stripe", payload, PENDING));
eventProcessor.processAsync(event.getId());
return ResponseEntity.ok().build();Rule 3: Idempotency Key
@Transactional
public void processEvent(String eventId) {
var existing = webhookEventRepo.findByExternalId(eventId);
if (existing.isPresent() && existing.get().getStatus() == PROCESSED) {
return; // Already handled
}
doProcess(existing.orElseThrow());
}Rule 4: Reconciliation
Webhooks can be lost. Run a daily reconciliation:
@Scheduled(cron = "0 0 3 * * *")
public void reconcilePayments() {
var pending = orderRepo.findByStatusAndCreatedBefore(
PENDING_PAYMENT, Instant.now().minus(Duration.ofHours(2))
);
for (var order : pending) {
var intent = stripe.paymentIntents().retrieve(order.getStripePaymentId());
if ("succeeded".equals(intent.getStatus())) {
fulfillOrder(intent);
}
}
}The Complete Pattern
- Receive webhook, verify signature
- Store the raw event with unique external ID
- Return 200 immediately
- Process asynchronously with idempotency check
- Reconcile daily for missed events
spring-bootwebhookspaymentsidempotency
Comments
Stay Updated
Get my latest articles delivered straight to your inbox.
Related Articles
Backend15 min read
Event-Driven Microservices with Spring Boot and Kafka
Patterns and pitfalls I encountered running event-sourced services at scale with Spring Boot and Apache Kafka.
B
Backend10 min read
Custom Spring Boot Starters: Packaging Shared Infrastructure
Stop copy-pasting configs across services — build auto-configured starters your team will thank you for.
B