Idempotência em APIs Spring Boot: chaves, Redis e exemplo passo a passo
Por John Pires · 20 de março de 2026

Introducao ao problema
Idempotência é um conceito pequeno na teoria e grande na prática. Em APIs HTTP, significa que repetir a mesma requisição não deve produzir efeitos colaterais adicionais. Para quem inicia, a analogia é simples: imagine apertar um botão "pagar" numa página de checkout. Se o cliente apertar duas vezes por acidente, você não quer dois pedidos cobrados. A idempotência garante que o segundo clique não gere um segundo pagamento.
Na prática, idempotência envolve identificar uma requisição de forma única, armazenar o resultado da primeira execução e evitar que execuções subsequentes façam a mesma operação. Para sistemas distribuídos e microservices, isso evita duplicidade em bancos, filas e integradores externos.
Há várias estratégias: operações naturalmente idempotentes (GET, DELETE em alguns casos), checagem por atributos do payload (por exemplo, valor + timestamp), e chaves de idempotência (geradas pelo cliente e validadas no servidor). Este artigo explora a estratégia de chaves, usando Redis como armazenamento leve para controle e armazenamento de respostas.
O que você vai aprender
Vou apresentar um fluxo completo, desde setup até implementação em Spring Boot, com um interceptor que valida a chave "Idempotency-Key" usando Redis e armazena a resposta para replays. Incluo código real e executável, dicas de armadilhas e um desafio prático ao final.
Por que usar Redis
Redis é um armazenamento em memória, rápido e com comandos atômicos (SETNX, EXPIRE). Essas propriedades tornam-no ideal para chaves de idempotência: você precisa garantir que apenas uma requisição com a mesma chave prossiga ao processamento, e Redis permite fazer isso de forma consistente e performática.
Dica: Redis é usado aqui como um cache de coordenação. Para operações críticas com durabilidade absoluta, combine com persistência transacional no destino final (banco relacional, por exemplo).
Pre-requisitos e setup
Antes de codificar, prepare o ambiente. Você precisa de:
- Java 17+ (ou 11 se preferir)
- Spring Boot 2.7+/3.x com Spring Web
- Spring Data Redis (ou Lettuce como client padrão)
- Redis em execução local ou acessível (Docker recomendado)
Se usar Docker, um comando rápido para levantar Redis:
docker run --name redis -p 6379:6379 -d redis:7
Esse container fornece um Redis simples para testes. Em produção, use cluster, autenticação e configurações de persistência adequadas.
Dependências Maven
Exemplo resumido do pom.xml com as dependências relevantes. Antes do código: ele adiciona Spring Web, Spring Data Redis e Spring Boot Starter.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
Depois de adicionar dependências, configure application.properties para apontar para o Redis de teste.
spring.redis.host=localhost
spring.redis.port=6379
# spring.redis.password=... se houver
Tutorial guiado passo a passo
Vamos construir uma API POST simples que cria um recurso "Order". O foco não é JPA complexo, mas a integração da idempotência usando Redis e Spring.
1) Configuração do Redis no Spring
Contexto: precisamos de um bean de StringRedisTemplate para realizar operações simples de string e comandos atômicos SETNX. O código abaixo cria o bean; depois explico o que faz.
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration
public class RedisConfig {
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
return new StringRedisTemplate(connectionFactory);
}
}
O que acontece: StringRedisTemplate encapsula operações de string no Redis. Vamos usá-lo para SETNX (setIfAbsent) e para salvar o resultado da resposta serializada.
2) Interceptor de Idempotência
Contexto: criamos um HandlerInterceptor que verifica o header "Idempotency-Key" antes do controller ser executado. Se a chave for nova, ele reserva a chave no Redis com um TTL; se já existir, retorna o resultado previamente armazenado (ou um conflito).
package com.example.idempotency;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Component
public class IdempotencyInterceptor implements HandlerInterceptor {
private final StringRedisTemplate redis;
private final Duration keyTtl = Duration.ofMinutes(5);
public IdempotencyInterceptor(StringRedisTemplate redis) {
this.redis = redis;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!"POST".equalsIgnoreCase(request.getMethod())) {
return true; // só aplicamos em POST para este exemplo
}
String key = request.getHeader("Idempotency-Key");
if (key == null || key.isBlank()) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing Idempotency-Key header");
return false;
}
String reserved = redis.opsForValue().get(key + ":status");
if (reserved != null) {
// já existe resultado: recuperamos e retornamos
String code = redis.opsForValue().get(key + ":code");
String body = redis.opsForValue().get(key + ":body");
int status = code != null ? Integer.parseInt(code) : HttpServletResponse.SC_OK;
response.setStatus(status);
if (body != null) {
response.setContentType("application/json");
response.getWriter().write(body);
}
return false; // intercepta a execução do controller
}
// tenta reservar a chave atomicalmente
Boolean success = redis.opsForValue().setIfAbsent(key + ":lock", "locked", keyTtl);
if (Boolean.FALSE.equals(success)) {
response.sendError(HttpServletResponse.SC_CONFLICT, "Request in progress or duplicate");
return false;
}
// continua para o controller
return true;
}
}
O que acontece: usamos chaves com sufixos ":lock", ":body" e ":code". Primeiro verificamos se já existe um resultado, retornando-o. Se não, tentamos reservar com setIfAbsent. Se a reserva falhar, entendemos que outra requisição com a mesma chave está em curso.
3) Controller e armazenamento da resposta
Contexto: o controller processa a criação do pedido. Após processamento, persiste o resultado no Redis para replays futuros e remove o lock se necessário. Aqui mostramos um controller simplificado sem JPA para clareza.
package com.example.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
@RestController
public class OrderController {
record OrderRequest(String productId, int quantity) {}
record OrderResponse(String orderId, String status) {}
private final StringRedisTemplate redis;
private final ObjectMapper jackson = new ObjectMapper();
private final Duration resultTtl = Duration.ofHours(1);
public OrderController(StringRedisTemplate redis) {
this.redis = redis;
}
@PostMapping("/orders")
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest req, @RequestHeader("Idempotency-Key") String key) throws Exception {
// Simula processamento e geração de pedido
String orderId = "ORD-" + System.currentTimeMillis();
OrderResponse resp = new OrderResponse(orderId, "CREATED");
// Serializa e guarda no Redis para futuros replays
String body = jackson.writeValueAsString(resp);
redis.opsForValue().set(key + ":body", body, resultTtl);
redis.opsForValue().set(key + ":code", String.valueOf(201), resultTtl);
redis.opsForValue().set(key + ":status", "done", resultTtl);
// opcional: remover lock
redis.delete(key + ":lock");
return ResponseEntity.status(201).body(resp);
}
}
O que acontece: depois do processamento, salvamos o corpo da resposta e o status. Requisições futuras com a mesma chave irão retornar esse corpo sem criar um novo pedido.
4) Registrando o Interceptor
Contexto: precisamos registrar o IdempotencyInterceptor no pipeline do Spring MVC para que ele seja invocado antes dos controllers.
package com.example.config;
import com.example.idempotency.IdempotencyInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private IdempotencyInterceptor idempotencyInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(idempotencyInterceptor);
}
}
Agora o fluxo está completo: o interceptor valida/reserva a chave, o controller persiste o resultado, e futuras chamadas leem o resultado do Redis.
Erros comuns, armadilhas e dicas práticas
Quando aplicar idempotência com chaves e Redis, há várias armadilhas que iniciantes encontram com frequência. Listo as principais com explicações e soluções.
Erros comuns
- Nao enviar Idempotency-Key: clientes esquecem de enviar o header. Solução: rejeitar com 400 e documentar a API.
- Tempo de vida da chave insuficiente: se TTL muito curto, chamadas repetidas após expiração vão criar duplicatas. Ajuste o TTL ao fluxo de negócio.
- Armazenar resposta parcial: se você salva no Redis antes de confirmar persistência em banco, pode haver inconsistência. Garanta ordem: confirmar no destino final, então salvar o resultado.
Dicas práticas
Use chaves geradas pelo cliente. Um UUID v4 no header funciona bem. Para segurança, também aceite chaves assinadas se houver risco de exploração por terceiros.
Observação: em integrações de pagamento, combine a chave com um registro transacional no banco (ex: unique constraint sobre idempotency_key) para garantia forte.
Resultado final funcionando
Com os componentes montados, testar é simples. O fluxo esperado:
- Cliente envia POST /orders com header Idempotency-Key: abc-123
- Interceptor tenta reservar abc-123:lock com SETNX
- Se bem-sucedido, controller processa e salva body/code/status no Redis
- Chamadas subsequentes com abc-123 retornam o body salvo
Exemplo de teste via curl. Antes do comando: certifique-se que o app está em execução.
curl -v -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-key-001" \
-d '{"productId":"P1","quantity":2}'
Se você executar o curl novamente com a mesma chave, receberá o mesmo body e não haverá criação de novo pedido. Logs e dados no Redis confirmam que a segunda chamada foi respondida pelo cache.
Como verificar no Redis
Conecte no Redis CLI e observe as chaves criadas:
127.0.0.1:6379> keys "my-key-001*"
1) "my-key-001:body"
2) "my-key-001:code"
3) "my-key-001:status"
Isso confirma que o resultado foi persistido. Se quiser depurar o lock, verifique a chave my-key-001:lock durante o processamento.
Desafio para o leitor praticar
Agora que tem a base, proponho exercícios práticos para consolidar o aprendizado. São tarefas progressivas e focadas em problemas reais.
- Modificar o exemplo para usar JPA e persistir Order em banco. Garanta que o salvamento no banco ocorra antes de gravar o resultado no Redis.
- Alterar a estratégia: ao invés de salvar corpo no Redis, salve apenas um identificador da entidade persistida e retorne via nova consulta. Compare performance e consistência.
- Implementar cleanup: crie um job que apague resultados antigos do Redis para economizar memória (considerar TTL já aplica isso, mas explore casos de falha).
- Tratamento de falhas: e se o processamento falhar depois de reservar a chave? Implemente um mecanismo que marque a chave como "failed" para permitir retry ou liberação do lock.
Esses exercícios ajudam a entender trade-offs entre consistência e performance e a adaptar a solução para requisitos reais.
Conclusao
Idempotência evita duplicação de efeitos colaterais em APIs e é essencial em pagamentos, criação de recursos e integrações com sistemas externos. A estratégia de chaves, combinada com Redis, é uma solução prática e escalável para muitas aplicações.
Resumo prático: use um header como "Idempotency-Key", reserve a chave no Redis com setIfAbsent, processe a requisição, armazene o resultado e retorne sempre o resultado salvo para chamadas repetidas. Tenha cuidado com TTL, ordem de persistência e tratamento de falhas.
Implemente os desafios propostos para migrar de um exemplo educacional para um caso de produção: junte JPA, trate falhas, proteja chaves e dimensione o Redis adequadamente. A idempotência é simples no conceito, mas exige disciplina na implementação.
Boa prática: documente claramente o comportamento da API, incluindo header obrigatório, política de TTL e o que acontece em caso de conflito. Isso reduz erros do lado do cliente e facilita debugging.
Por fim, experimente variações: armazenamento de respostas parciais, chaves assinadas, rate limiting combinado com idempotência. Cada ajuste resolve requisitos específicos — entenda as garantias que seu negócio precisa.
Assuntos: spring-boot, idempotencia, NoSQL, Java
Leia também
Instrumentando Spring Boot com OpenTelemetry: traces, métricas e logs passo a passo
Guia prático para instrumentar Spring Boot com OpenTelemetry: do ambiente à instrumentação manual e via javaagent, traces, métricas e correlação de logs com…
JPA Embeddables: criar e usar Value Objects (Address, Money) passo a passo
Aprenda passo a passo a criar e usar JPA Embeddables para value objects (Address, Money). Exemplos Java, boas práticas, armadilhas comuns e código funcional…
Migrando endpoints bloqueantes para Virtual Threads no Spring Boot (passo a passo)
Guia prático para migrar endpoints bloqueantes no Spring Boot para virtual threads (Java 21). Padrões, código funcional, tratamento de transações, segurança e…
Rate limiting por usuário em Spring Boot com Redis (Token Bucket) — passo a passo
Aprenda a implementar rate limiting por usuário em Spring Boot usando Redis e Token Bucket. Mostro a abordagem errada (in-memory), a solução correta com…
Virtual Threads no Spring Boot: crie uma API concorrente passo a passo
Aprenda a usar virtual threads com Spring Boot para criar APIs concorrentes sem reescrever tudo. Veja code-review: o jeito errado, o certo, configuração…
Multi-tenant em Spring Boot: schema-per-tenant com Hibernate, Flyway e routing
Implementação prática de schema-per-tenant em Spring Boot: routing do tenant, configuração Hibernate (MultiTenantConnectionProvider), e aplicação de migrações…