java
59 lines · 10 steps
Building an idempotency filter with Redis in Spring
A servlet filter that uses Redis to make POST requests safe to retry by replaying the stored response for duplicate keys.
Explained by
highlit
1@Component
2public class IdempotencyFilter extends OncePerRequestFilter {
3
4 private final StringRedisTemplate redis;
5 private final ObjectMapper objectMapper;
6 private static final Duration TTL = Duration.ofHours(24);
7
8 public IdempotencyFilter(StringRedisTemplate redis, ObjectMapper objectMapper) {
9 this.redis = redis;
10 this.objectMapper = objectMapper;
11 }
12
13 @Override
14 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
15 FilterChain chain) throws ServletException, IOException {
16 if (!"POST".equalsIgnoreCase(request.getMethod())) {
17 chain.doFilter(request, response);
18 return;
19 }
20
21 String key = request.getHeader("Idempotency-Key");
22 if (key == null || key.isBlank()) {
23 chain.doFilter(request, response);
24 return;
25 }
26
27 String cacheKey = "idem:" + key;
28 Boolean claimed = redis.opsForValue().setIfAbsent(cacheKey, "IN_PROGRESS", TTL);
29
30 if (Boolean.FALSE.equals(claimed)) {
31 String cached = redis.opsForValue().get(cacheKey);
32 if (cached == null || "IN_PROGRESS".equals(cached)) {
33 response.setStatus(HttpStatus.CONFLICT.value());
34 response.setContentType(MediaType.APPLICATION_JSON_VALUE);
35 response.getWriter().write("{\"error\":\"request already in progress\"}");
36 return;
37 }
38 StoredResponse stored = objectMapper.readValue(cached, StoredResponse.class);
39 response.setStatus(stored.status());
40 response.setContentType(MediaType.APPLICATION_JSON_VALUE);
41 response.getWriter().write(stored.body());
42 return;
43 }
44
45 ContentCachingResponseWrapper wrapper = new ContentCachingResponseWrapper(response);
46 try {
47 chain.doFilter(request, wrapper);
48 String body = new String(wrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
49 StoredResponse stored = new StoredResponse(wrapper.getStatus(), body);
50 redis.opsForValue().set(cacheKey, objectMapper.writeValueAsString(stored), TTL);
51 wrapper.copyBodyToResponse();
52 } catch (Exception e) {
53 redis.delete(cacheKey);
54 throw e;
55 }
56 }
57
58 private record StoredResponse(int status, String body) {}
59}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1An atomic setIfAbsent turns a cache into a claim lock, letting only the first request execute while duplicates wait or replay.
- 2Persisting status and body lets a retried request receive the exact original response instead of running the handler twice.
- 3Deleting the claim on failure keeps a crashed request from permanently blocking retries of the same key.
Related explainers
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
java
public static Map<String, String> parseCookieHeader(String header) { Map<String, String> cookies = new LinkedHashMap<>(); if (header == null || header.isBlank()) { return cookies;
Parsing an HTTP Cookie header in Java
string-parsing
http
url-decoding
Intermediate
6 steps
python
import secrets from django.contrib.auth import authenticate, login from django.core.cache import cache
Two-factor login with OTP in Django
two-factor-auth
one-time-passwords
caching
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-an-idempotency-filter-with-redis-in-spring-explained-java-3cd7/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.