Integrar dois sistemas via pedidos parece a tarefa mais banal do mundo: recebe um JSON, salva no banco, devolve 201. Você faz isso antes do café.
O problema nunca é esse. O problema aparece três semanas depois, em produção, quando o sistema que te envia os pedidos sofre um timeout, reenvia a mesma requisição, e você descobre que o pedido A-123456 agora existe duas vezes no seu banco — com cobrança dobrada, estoque furado e um cliente irritado.
Este artigo é sobre como receber pedidos de forma idempotente, mesmo sob concorrência, em Java + Spring Boot. É a diferença entre um endpoint que funciona na demo e um que aguenta produção.
O cenário
Um Sistema A envia pedidos para o seu serviço. Cada pedido tem um identificador de origem — vou chamar de externalOrderId. Seu serviço precisa:
- Receber o pedido (
POST /orders). - Calcular os totais.
- Persistir.
- Disponibilizar para consulta de um Sistema B.
A regra de ouro: o mesmo externalOrderId nunca pode virar dois pedidos, não importa quantas vezes o Sistema A reenvie, nem quantas instâncias do seu serviço estejam rodando.
A tentativa ingênua (que falha)
A primeira versão que todo mundo escreve é mais ou menos esta:
public Order receber(OrderRequest req) {
Optional<Order> existente = repo.findByExternalOrderId(req.externalOrderId());
if (existente.isPresent()) {
return existente.get(); // já existe, retorna
}
Order novo = calcular(req);
return repo.save(novo); // cria
}
Parece correto. E funciona — até duas requisições com o mesmo externalOrderId chegarem ao mesmo tempo (duas instâncias, ou duas threads). As duas executam o findBy..., as duas não encontram nada, as duas chamam save. Resultado: pedido duplicado.
Esse é um race condition clássico. O check-then-act (verifico e depois ajo) não é atômico, então a verificação não vale nada sob concorrência.
A solução correta: deixe o banco garantir a unicidade
A regra de "um por externalOrderId" não deve viver só no código — ela tem que ser uma constraint do banco:
ALTER TABLE orders ADD CONSTRAINT uq_orders_external_id UNIQUE (external_order_id);
Com isso, mesmo que duas transações tentem inserir o mesmo pedido simultaneamente, o banco rejeita a segunda. Aí o seu código só precisa tratar essa rejeição como "ah, já existe" — e devolver o pedido existente:
public Result receber(OrderRequest req) {
try {
Order novo = calcular(req);
repo.saveAndFlush(novo); // flush para a violação estourar AGORA
return Result.criado(novo); // -> 201 Created
} catch (DataIntegrityViolationException e) {
// outra requisição ganhou a corrida: o pedido já existe
Order existente = repo.findByExternalOrderId(req.externalOrderId())
.orElseThrow();
return Result.jaExistia(existente); // -> 200 OK
}
}
Repare na semântica de status:
- 201 Created quando você criou o pedido.
- 200 OK quando o pedido já existia (a requisição é idempotente — repetir não muda nada).
Essa distinção é importante: o Sistema A pode reenviar à vontade que o comportamento é sempre seguro e previsível. É exatamente o que se espera de uma API resiliente.
Totais: cuidado com double
Outro detalhe que parece bobo e quebra em produção: dinheiro não é double. 0.1 + 0.2 não dá 0.3 em ponto flutuante. Para totais de pedido, use BigDecimal com escala e arredondamento explícitos:
BigDecimal lineTotal = unitPrice
.multiply(BigDecimal.valueOf(quantity))
.setScale(2, RoundingMode.HALF_UP);
BigDecimal totalAmount = items.stream()
.map(Item::lineTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.setScale(2, RoundingMode.HALF_UP);
Escala 2, HALF_UP, sempre. Assim o total fecha igual ao da planilha do financeiro — e você não vira o dev do "centavo sumido".
Observabilidade: o que salva você às 3 da manhã
Quando algo der errado (e vai), você precisa rastrear uma requisição do começo ao fim. Um correlation id propagado por header e logado em tudo resolve 80% das investigações:
String correlationId = Optional.ofNullable(request.getHeader("X-Correlation-Id"))
.orElse(UUID.randomUUID().toString());
MDC.put("correlationId", correlationId);
Some a isso o Actuator (/actuator/health) e logs estruturados, e você consegue operar com confiança em vez de no escuro.
Recapitulando
Uma ingestão de pedidos pronta para produção precisa de:
- Unicidade no banco (constraint), não só no código.
- Tratar a violação como idempotência (201 ao criar, 200 quando já existe).
-
BigDecimalcom escala e arredondamento explícitos para dinheiro. - Correlation id + observabilidade para conseguir investigar produção.
Nenhuma dessas peças é difícil isoladamente. O trabalho está em juntar tudo, testar sob concorrência e documentar as decisões para o time não desfazer depois.
Quer pular essa parte?
Eu empacotei exatamente esse padrão num template de produção em Java 21 + Spring Boot 3: ingestão idempotente sob concorrência, cálculo correto de totais, PostgreSQL + Liquibase, observabilidade, ADRs e runbook — sobe com um docker compose up e já vem com scripts que simulam os sistemas externos.
Em vez de gastar semanas montando a fundação, você parte dela.
👉 Conheça o Order Service Template
Felipe Ricarte Magalhães — Arquiteto de Software (hands-on), +17 anos em backend e integrações corporativas.











