💻 Exemplos: Capítulo 20: Projeto Corporativo: Microsserviço de Pagamentos com Auditoria JSON
Especialização em Backend com Java & Spring Boot • Demonstrações Práticas Compiláveis
💻 Código de Exemplo 01 — Verificação Atômica de Idempotência com Redis
package com.empresa.api.service;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
@Service
public class IdempotencyService {
private final StringRedisTemplate redisTemplate;
public IdempotencyService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public boolean verificarEBloquear(String chave) {
// SETNX atômico: só uma requisição concorrente "ganha" e processa; as demais
// encontram a chave já ocupada e sabem que é uma repetição em andamento.
Boolean adquiriu = redisTemplate.opsForValue()
.setIfAbsent("idempotency:" + chave, "PROCESSANDO", Duration.ofMinutes(10));
return Boolean.TRUE.equals(adquiriu);
}
public void concluir(String chave, String respostaSerializada) {
redisTemplate.opsForValue().set("idempotency:" + chave, respostaSerializada, Duration.ofHours(24));
}
public String buscarRespostaOriginal(String chave) {
return redisTemplate.opsForValue().get("idempotency:" + chave);
}
}
package com.empresa.api.controller;
import com.empresa.api.service.IdempotencyService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PagamentoController {
private final IdempotencyService idempotencyService;
private final PagamentoOrquestradorService orquestradorService;
public PagamentoController(IdempotencyService idempotencyService,
PagamentoOrquestradorService orquestradorService) {
this.idempotencyService = idempotencyService;
this.orquestradorService = orquestradorService;
}
@PostMapping("/api/v1/pagamentos")
public Object processar(@RequestHeader("Idempotency-Key") String idempotencyKey,
@org.springframework.web.bind.annotation.RequestBody NovoPagamentoRequest request) {
if (!idempotencyService.verificarEBloquear(idempotencyKey)) {
// Requisição duplicada: devolve o resultado original sem cobrar novamente.
return idempotencyService.buscarRespostaOriginal(idempotencyKey);
}
return orquestradorService.processar(idempotencyKey, request.pedidoId(), request.valor());
}
}
record NovoPagamentoRequest(Long pedidoId, java.math.BigDecimal valor) {}
💻 Código de Exemplo 02 — Auditoria Imutável com Hash Encadeado
package com.empresa.api.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;
import java.security.MessageDigest;
import java.time.Instant;
@Service
public class AuditPaymentService {
private final AuditoriaRepository auditoriaRepository;
private final ObjectMapper objectMapper;
public AuditPaymentService(AuditoriaRepository auditoriaRepository, ObjectMapper objectMapper) {
this.auditoriaRepository = auditoriaRepository;
this.objectMapper = objectMapper;
}
public void registrarAuditoriaPagamento(Long transacaoId, String status) {
try {
String hashAnterior = auditoriaRepository.buscarUltimoHash();
var registro = new RegistroAuditoria(transacaoId, status, Instant.now().toString(), hashAnterior);
String jsonRegistro = objectMapper.writeValueAsString(registro);
String hashAtual = calcularHash(jsonRegistro);
// Cada registro referencia o hash do anterior: qualquer adulteração de um
// registro passado quebra a cadeia de hashes de todos os registros seguintes.
auditoriaRepository.salvar(jsonRegistro, hashAtual);
} catch (Exception ex) {
throw new IllegalStateException("Falha ao gravar auditoria imutável", ex);
}
}
private String calcularHash(String conteudo) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(conteudo.getBytes(java.nio.charset.StandardCharsets.UTF_8));
return java.util.HexFormat.of().formatHex(hashBytes);
}
}
record RegistroAuditoria(Long transacaoId, String status, String timestamp, String hashAnterior) {}
💻 Código de Exemplo 03 — Teste E2E do Fluxo Completo de Pagamento
package com.empresa.api;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
class PagamentoFluxoCompletoE2ETest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@org.springframework.test.context.DynamicPropertySource
static void datasource(org.springframework.test.context.DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired private MockMvc mockMvc;
@Test
void deveProcessarPagamentoComIdempotenciaAuditoriaEEventoPublicado() throws Exception {
String idempotencyKey = java.util.UUID.randomUUID().toString();
String corpoRequisicao = """
{"pedidoId": 501, "valor": 199.90}
""";
// 1ª chamada: processa de fato (autenticação, circuit breaker, auditoria, evento Kafka).
mockMvc.perform(post("/api/v1/pagamentos")
.header("Idempotency-Key", idempotencyKey)
.contentType(MediaType.APPLICATION_JSON)
.content(corpoRequisicao))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("APROVADO"));
// 2ª chamada com a MESMA chave: deve retornar o resultado original, sem cobrar de novo.
mockMvc.perform(post("/api/v1/pagamentos")
.header("Idempotency-Key", idempotencyKey)
.contentType(MediaType.APPLICATION_JSON)
.content(corpoRequisicao))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("APROVADO"));
}
}