Java 11+ e Spring Boot: Construindo Sistemas Bancários Modernos
Como usar features modernas do Java 11+ e Spring Boot para construir sistemas bancários performáticos e escaláveis
Java 11+ e Spring Boot: Construindo Sistemas Bancários Modernos
Para quem está começando: explicação simples
Por que Java 11+ para Sistemas Bancários?
Imagine que você tem um sistema financeiro que precisa:
Velocidade:
- Processar 1 milhão de transações PIX por segundo
- Responder consultas em menos de 50ms
- Executar regras de negócio complexas rapidamente
Confiabilidade:
- Nunca errar nos cálculos (dinheiro é sagrado!)
- Logs detalhados para auditoria
- Restart automático se algo der errado
Escalabilidade:
- Funcionar no Black Friday (10x mais operações)
- Adicionar novas funcionalidades facilmente
- Suportar milhares de consultas simultâneas
Java 8 vs Java 11+ para Sistemas Financeiros
Java 8 (legado):
- Como usar uma calculadora antiga
- Funciona, mas é lenta e limitada
- Código verboso para regras simples
- Performance limitada
Java 11+ (moderno):
- Como usar uma calculadora científica moderna
- Muito mais rápida e inteligente
- Código limpo e expressivo
- Performance otimizada para cálculos complexos
Spring Boot para Sistemas Bancários
Sem Spring Boot:
- Como construir um prédio tijolo por tijolo
- Configurar tudo manualmente: banco, segurança, logs
- Muito trabalho antes de implementar a primeira funcionalidade
Com Spring Boot:
- Como usar blocos de LEGO prontos
- Configuração automática: database, APIs, monitoring
- Foco total nas regras de negócio
Analogia: Sistema Bancário
Sistema Tradicional (Java 8):
- Processos manuais e burocráticos
- Cálculos lentos e propensos a erro
- Um sistema monolítico faz tudo
- Gargalos nos horários de pico
Sistema Moderno (Java 11+ + Spring Boot):
- Interface digital inteligente
- Cálculos automáticos e precisos
- Microsserviços especializados
- Autoscaling: mais recursos quando precisa
Por que isso importa?
Receita: Processamento mais rápido = mais operações = mais receita Precisão: Menos bugs = menos perda de dinheiro Insights: Dados melhores = decisões de negócio mais inteligentes Inovação: Tempo economizado = foco em novas funcionalidades
Conceitos técnicos
Evolução do Java para Sistemas Financeiros
Java 11+ trouxe melhorias significativas:
HTTP Client nativo: Consultas a APIs externas sem dependências Local Variable Type Inference: Código mais limpo e legível Flight Recorder: Profiling detalhado de performance Text Blocks: Queries SQL mais legíveis Pattern Matching: Lógica de negócio mais expressiva Records: DTOs imutáveis para transferência de dados
Spring Boot 3.x
Native Compilation: Startup ultra-rápido para microserviços Observability: Métricas automáticas de performance Security: Proteção robusta para APIs sensíveis Data: Integração simplificada com bancos de dados
Arquitetura: Sistema Bancário Moderno
flowchart TB
subgraph "Sistema Bancário Java 11+ + Spring Boot"
subgraph "API Layer"
GATEWAY[API Gateway]
CALC_API[Processing API]
QUERY_API[Query API]
ADMIN_API[Admin API]
end
subgraph "Business Layer"
CALC_SERVICE[Calculation Service]
RULES_ENGINE[Rules Engine]
PRICING_SERVICE[Pricing Service]
VALIDATION[Validation Service]
end
subgraph "Data Layer"
BUSINESS_DB[(Business Database)]
CACHE[Redis Cache]
AUDIT_LOG[(Audit Log)]
end
subgraph "Observability"
METRICS[Micrometer Metrics]
TRACING[Distributed Tracing]
LOGS[Structured Logs]
end
subgraph "Infrastructure"
CONFIG[Config Server]
DISCOVERY[Service Discovery]
CIRCUIT_BREAKER[Circuit Breakers]
end
end
GATEWAY --> CALC_API
GATEWAY --> QUERY_API
GATEWAY --> ADMIN_API
CALC_API --> CALC_SERVICE
QUERY_API --> PRICING_SERVICE
ADMIN_API --> RULES_ENGINE
CALC_SERVICE --> RULES_ENGINE
CALC_SERVICE --> VALIDATION
PRICING_SERVICE --> CACHE
CALC_SERVICE --> BUSINESS_DB
PRICING_SERVICE --> BUSINESS_DB
RULES_ENGINE --> AUDIT_LOG
CALC_API --> METRICS
QUERY_API --> TRACING
ADMIN_API --> LOGS
CALC_SERVICE --> CIRCUIT_BREAKER
PRICING_SERVICE --> CONFIG
RULES_ENGINE --> DISCOVERY
style CALC_SERVICE fill:#e3f2fd
style RULES_ENGINE fill:#f3e5f5
style CACHE fill:#e8f5e8
style METRICS fill:#fff3e0
Features Java 11+ na Prática
1. Text Blocks para Queries Complexas
// Antes (Java 8) - SQL difícil de ler
@Repository
public class TarifaRepositoryOld {
private static final String QUERY_TARIFAS =
"SELECT t.id, t.operacao_tipo, t.valor_minimo, t.valor_maximo, " +
"t.tarifa_fixa, t.tarifa_percentual, t.data_vigencia " +
"FROM tarifas t " +
"INNER JOIN regras_tarifa rt ON t.id = rt.tarifa_id " +
"WHERE t.operacao_tipo = ? " +
"AND t.data_vigencia <= CURRENT_DATE " +
"AND (t.data_expiracao IS NULL OR t.data_expiracao > CURRENT_DATE) " +
"AND rt.conta_tipo = ? " +
"ORDER BY t.prioridade DESC";
}
// Depois (Java 11+) - SQL legível e organizado
@Repository
public class TarifaRepository {
private static final String QUERY_TARIFAS = """
SELECT t.id, t.operacao_tipo, t.valor_minimo, t.valor_maximo,
t.tarifa_fixa, t.tarifa_percentual, t.data_vigencia
FROM tarifas t
INNER JOIN regras_tarifa rt ON t.id = rt.tarifa_id
WHERE t.operacao_tipo = ?
AND t.data_vigencia <= CURRENT_DATE
AND (t.data_expiracao IS NULL OR t.data_expiracao > CURRENT_DATE)
AND rt.conta_tipo = ?
ORDER BY t.prioridade DESC
""";
@Query(value = QUERY_TARIFAS, nativeQuery = true)
List<Tarifa> findTarifasVigentes(TipoOperacao operacao, TipoConta conta);
}
2. Records para DTOs Bancários
// Antes (Java 8) - Muito código boilerplate
public class TarifaCalculationRequest {
private final TipoOperacao operacao;
private final BigDecimal valor;
private final TipoConta tipoConta;
private final String clienteId;
private final LocalDateTime timestamp;
public TarifaCalculationRequest(TipoOperacao operacao, BigDecimal valor,
TipoConta tipoConta, String clienteId,
LocalDateTime timestamp) {
this.operacao = operacao;
this.valor = valor;
this.tipoConta = tipoConta;
this.clienteId = clienteId;
this.timestamp = timestamp;
}
// 50+ linhas de getters, equals, hashCode, toString...
}
// Depois (Java 14+) - Código limpo e conciso
public record TarifaCalculationRequest(
TipoOperacao operacao,
BigDecimal valor,
TipoConta tipoConta,
String clienteId,
LocalDateTime timestamp
) {
// Validações no construtor compacto
public TarifaCalculationRequest {
Objects.requireNonNull(operacao, "Operação não pode ser nula");
Objects.requireNonNull(valor, "Valor não pode ser nulo");
if (valor.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Valor deve ser positivo");
}
Objects.requireNonNull(tipoConta, "Tipo de conta não pode ser nulo");
Objects.requireNonNull(clienteId, "Cliente ID não pode ser nulo");
Objects.requireNonNull(timestamp, "Timestamp não pode ser nulo");
}
// Métodos de conveniência
public boolean isPixOperacao() {
return operacao == TipoOperacao.PIX;
}
public boolean isValorAlto() {
return valor.compareTo(new BigDecimal("10000")) > 0;
}
}
public record TarifaCalculationResponse(
BigDecimal tarifaCalculada,
String regraAplicada,
boolean isencaoAplicada,
String motivoIsencao,
LocalDateTime calculadoEm
) {}
3. Pattern Matching para Regras de Negócio
// Antes (Java 8) - if/else verboso
@Service
public class TarifaCalculatorOld {
public BigDecimal calcularTarifa(TarifaCalculationRequest request) {
if (request.getOperacao() == TipoOperacao.PIX) {
if (request.getValor().compareTo(new BigDecimal("1000")) <= 0) {
return BigDecimal.ZERO; // PIX até 1000 é gratuito
} else {
return new BigDecimal("1.50");
}
} else if (request.getOperacao() == TipoOperacao.TED) {
if (request.getTipoConta() == TipoConta.PREMIUM) {
return BigDecimal.ZERO; // Premium isento
} else {
return new BigDecimal("12.50");
}
} else if (request.getOperacao() == TipoOperacao.SAQUE_ATM) {
// Mais ifs aninhados...
}
return BigDecimal.ZERO;
}
}
// Depois (Java 17+) - Pattern matching elegante
@Service
public class TarifaCalculator {
public BigDecimal calcularTarifa(TarifaCalculationRequest request) {
return switch (request.operacao()) {
case PIX -> calcularTarifaPix(request);
case TED -> calcularTarifaTed(request);
case SAQUE_ATM -> calcularTarifaSaque(request);
case TRANSFERENCIA -> calcularTarifaTransferencia(request);
case CARTAO_CREDITO -> calcularTarifaCartao(request);
};
}
private BigDecimal calcularTarifaPix(TarifaCalculationRequest request) {
return switch (request.tipoConta()) {
case PREMIUM -> BigDecimal.ZERO;
case CORRENTE -> request.valor().compareTo(new BigDecimal("1000")) <= 0
? BigDecimal.ZERO
: new BigDecimal("1.50");
case POUPANCA -> new BigDecimal("0.50");
};
}
private BigDecimal calcularTarifaTed(TarifaCalculationRequest request) {
return switch (request.tipoConta()) {
case PREMIUM -> BigDecimal.ZERO;
case CORRENTE, POUPANCA -> new BigDecimal("12.50");
};
}
// Métodos mais focados e legíveis
}
4. HTTP Client Nativo para APIs Externas
// Antes (Java 8) - Dependência externa (Apache HttpClient)
@Service
public class ValidadorCpfServiceOld {
private final CloseableHttpClient httpClient;
public boolean validarCpf(String cpf) {
try {
HttpGet request = new HttpGet("https://api.receita.fazenda.gov.br/cpf/" + cpf);
CloseableHttpResponse response = httpClient.execute(request);
String responseBody = EntityUtils.toString(response.getEntity());
return parseResponse(responseBody);
} catch (Exception e) {
throw new RuntimeException("Erro ao validar CPF", e);
}
}
}
// Depois (Java 11+) - HTTP Client nativo
@Service
public class ValidadorCpfService {
private final HttpClient httpClient;
public ValidadorCpfService() {
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
}
public CompletableFuture<Boolean> validarCpfAsync(String cpf) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.receita.fazenda.gov.br/cpf/" + cpf))
.timeout(Duration.ofSeconds(10))
.header("Content-Type", "application/json")
.GET()
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> {
if (response.statusCode() == 200) {
return parseResponse(response.body());
} else {
throw new RuntimeException("Erro HTTP: " + response.statusCode());
}
})
.exceptionally(throwable -> {
log.error("Erro ao validar CPF {}: {}", cpf, throwable.getMessage());
return false; // Fallback: considera inválido em caso de erro
});
}
// Versão síncrona quando necessário
public boolean validarCpf(String cpf) {
try {
return validarCpfAsync(cpf).get(10, TimeUnit.SECONDS);
} catch (Exception e) {
log.error("Timeout ao validar CPF {}", cpf);
return false;
}
}
}
5. Local Variable Type Inference (var)
// Antes (Java 8) - Tipos verbosos
@Service
public class TarifaStatisticsServiceOld {
public Map<TipoOperacao, BigDecimal> calcularReceitaPorOperacao(LocalDate data) {
List<TarifaCobranca> cobrancas = tarifaRepository.findByData(data);
Map<TipoOperacao, BigDecimal> receitaPorOperacao = new HashMap<>();
for (TarifaCobranca cobranca : cobrancas) {
BigDecimal receitaAtual = receitaPorOperacao.getOrDefault(
cobranca.getTipoOperacao(), BigDecimal.ZERO);
BigDecimal novaReceita = receitaAtual.add(cobranca.getValorTarifa());
receitaPorOperacao.put(cobranca.getTipoOperacao(), novaReceita);
}
return receitaPorOperacao;
}
}
// Depois (Java 10+) - Código mais limpo com var
@Service
public class TarifaStatisticsService {
public Map<TipoOperacao, BigDecimal> calcularReceitaPorOperacao(LocalDate data) {
var cobrancas = tarifaRepository.findByData(data);
return cobrancas.stream()
.collect(Collectors.groupingBy(
TarifaCobranca::tipoOperacao,
Collectors.reducing(
BigDecimal.ZERO,
TarifaCobranca::valorTarifa,
BigDecimal::add
)
));
}
public TarifaStatistics gerarEstatisticas(LocalDate inicio, LocalDate fim) {
var cobrancas = tarifaRepository.findByPeriodo(inicio, fim);
var totalReceita = cobrancas.stream()
.map(TarifaCobranca::valorTarifa)
.reduce(BigDecimal.ZERO, BigDecimal::add);
var operacaoMaisLucrativa = cobrancas.stream()
.collect(Collectors.groupingBy(
TarifaCobranca::tipoOperacao,
Collectors.reducing(BigDecimal.ZERO, TarifaCobranca::valorTarifa, BigDecimal::add)
))
.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(null);
var mediaValorPorOperacao = cobrancas.stream()
.collect(Collectors.groupingBy(
TarifaCobranca::tipoOperacao,
Collectors.averagingDouble(c -> c.valorTarifa().doubleValue())
));
return new TarifaStatistics(
totalReceita,
operacaoMaisLucrativa,
mediaValorPorOperacao,
cobrancas.size()
);
}
}
Spring Boot 3.x na Prática
1. API REST Moderna com Spring Boot
@RestController
@RequestMapping("/api/v1/tarifas")
@Validated
@Tag(name = "Tarifas", description = "APIs para cálculo e consulta de tarifas bancárias")
public class TarifaController {
private final TarifaService tarifaService;
private final TarifaValidationService validationService;
@PostMapping("/calcular")
@Operation(summary = "Calcula tarifa para uma operação",
description = "Calcula a tarifa aplicável baseada no tipo de operação, valor e tipo de conta")
public ResponseEntity<TarifaResponse> calcularTarifa(
@Valid @RequestBody TarifaCalculationRequest request,
@RequestHeader("X-Cliente-ID") String clienteId,
@RequestHeader(value = "X-Correlation-ID", required = false) String correlationId) {
// Validações de negócio
validationService.validarRequest(request, clienteId);
// Cálculo da tarifa
var tarifaCalculada = tarifaService.calcularTarifa(request, clienteId);
// Response com metadata
var response = TarifaResponse.builder()
.valor(tarifaCalculada.valor())
.regraAplicada(tarifaCalculada.regraAplicada())
.isencaoAplicada(tarifaCalculada.isencaoAplicada())
.calculadoEm(tarifaCalculada.timestamp())
.correlationId(correlationId)
.build();
return ResponseEntity.ok(response);
}
@GetMapping("/tabela/{tipoOperacao}")
@Operation(summary = "Consulta tabela de tarifas",
description = "Retorna as tarifas vigentes para um tipo de operação")
@Cacheable(value = "tabela-tarifas", key = "#tipoOperacao")
public ResponseEntity<List<TarifaTabelaItem>> consultarTabela(
@PathVariable @Parameter(description = "Tipo da operação") TipoOperacao tipoOperacao,
@RequestParam(defaultValue = "false") boolean incluirIsentas) {
var tabela = tarifaService.consultarTabela(tipoOperacao, incluirIsentas);
return ResponseEntity.ok(tabela);
}
@GetMapping("/historico/{clienteId}")
@Operation(summary = "Histórico de tarifas cobradas",
description = "Retorna histórico de tarifas cobradas de um cliente")
@PreAuthorize("hasRole('ADMIN') or #clienteId == authentication.name")
public ResponseEntity<Page<TarifaHistoricoItem>> historicoCliente(
@PathVariable String clienteId,
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate inicio,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fim) {
var pageable = PageRequest.of(page, size, Sort.by("dataCobranca").descending());
var historico = tarifaService.consultarHistorico(clienteId, inicio, fim, pageable);
return ResponseEntity.ok(historico);
}
@PostMapping("/simular")
@Operation(summary = "Simula cálculo de tarifa",
description = "Simula o cálculo sem efetuar cobrança (para frontend)")
public ResponseEntity<TarifaSimulacaoResponse> simularTarifa(
@Valid @RequestBody TarifaSimulacaoRequest request) {
var simulacao = tarifaService.simularTarifa(request);
return ResponseEntity.ok(simulacao);
}
@ExceptionHandler(TarifaNaoEncontradaException.class)
public ResponseEntity<ErrorResponse> handleTarifaNaoEncontrada(TarifaNaoEncontradaException ex) {
var error = ErrorResponse.builder()
.code("TARIFA_NAO_ENCONTRADA")
.message("Tarifa não encontrada para os parâmetros informados")
.details(ex.getMessage())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
2. Service Layer com Business Logic
@Service
@Transactional
@Validated
public class TarifaService {
private final TarifaRepository tarifaRepository;
private final TarifaCalculationEngine calculationEngine;
private final TarifaAuditService auditService;
private final ClienteService clienteService;
private final CacheManager cacheManager;
@Retryable(value = {DataAccessException.class}, maxAttempts = 3)
@CircuitBreaker(name = "tarifa-calculation", fallbackMethod = "calcularTarifaFallback")
@TimeLimiter(name = "tarifa-calculation")
public TarifaCalculationResult calcularTarifa(TarifaCalculationRequest request, String clienteId) {
// 1. Validar cliente e operação
var cliente = clienteService.findById(clienteId);
validarOperacaoPermitida(cliente, request.operacao());
// 2. Buscar regras de tarifa aplicáveis
var regrasAplicaveis = tarifaRepository.findRegrasVigentes(
request.operacao(),
cliente.getTipoConta(),
request.valor()
);
if (regrasAplicaveis.isEmpty()) {
throw new TarifaNaoEncontradaException(
"Nenhuma regra de tarifa encontrada para operação: " + request.operacao()
);
}
// 3. Aplicar engine de cálculo
var resultado = calculationEngine.calcular(request, regrasAplicaveis, cliente);
// 4. Verificar isenções
var isencao = verificarIsencoes(cliente, request, resultado);
if (isencao.isPresent()) {
resultado = resultado.comIsencao(isencao.get());
}
// 5. Registrar auditoria
auditService.registrarCalculoTarifa(request, resultado, clienteId);
// 6. Invalidar cache se necessário
if (resultado.regraAplicada().startsWith("PROMOCIONAL")) {
cacheManager.getCache("tabela-tarifas").evict(request.operacao());
}
return resultado;
}
public TarifaCalculationResult calcularTarifaFallback(TarifaCalculationRequest request,
String clienteId,
Exception ex) {
log.warn("Usando tarifa padrão devido a falha: {}", ex.getMessage());
// Tarifa padrão baseada na operação
var tarifaPadrao = switch (request.operacao()) {
case PIX -> new BigDecimal("1.00");
case TED -> new BigDecimal("10.00");
case SAQUE_ATM -> new BigDecimal("5.00");
default -> new BigDecimal("2.00");
};
return TarifaCalculationResult.builder()
.valor(tarifaPadrao)
.regraAplicada("TARIFA_PADRAO_FALLBACK")
.isencaoAplicada(false)
.timestamp(LocalDateTime.now())
.build();
}
@Cacheable(value = "tabela-tarifas", key = "#tipoOperacao + '-' + #incluirIsentas")
public List<TarifaTabelaItem> consultarTabela(TipoOperacao tipoOperacao, boolean incluirIsentas) {
var tarifas = tarifaRepository.findByTipoOperacaoAndVigente(tipoOperacao);
return tarifas.stream()
.filter(tarifa -> incluirIsentas || tarifa.getValor().compareTo(BigDecimal.ZERO) > 0)
.map(this::mapToTabelaItem)
.sorted(Comparator.comparing(TarifaTabelaItem::prioridade))
.collect(Collectors.toList());
}
private Optional<TarifaIsencao> verificarIsencoes(Cliente cliente,
TarifaCalculationRequest request,
TarifaCalculationResult resultado) {
// Isenção por tipo de conta
if (cliente.getTipoConta() == TipoConta.PREMIUM) {
return Optional.of(new TarifaIsencao("CONTA_PREMIUM", "Cliente conta premium"));
}
// Isenção por valor baixo (PIX até R$ 1000)
if (request.operacao() == TipoOperacao.PIX &&
request.valor().compareTo(new BigDecimal("1000")) <= 0) {
return Optional.of(new TarifaIsencao("PIX_VALOR_BAIXO", "PIX até R$ 1.000 isento"));
}
// Isenção por promoção ativa
var promocaoAtiva = promocaoService.findPromocaoAtiva(cliente.getId(), request.operacao());
if (promocaoAtiva.isPresent()) {
return Optional.of(new TarifaIsencao("PROMOCAO", promocaoAtiva.get().getDescricao()));
}
return Optional.empty();
}
}
3. Configuration e Security
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class TarifaSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/tarifas/simular").permitAll()
.requestMatchers("/api/v1/tarifas/tabela/**").hasRole("USER")
.requestMatchers("/api/v1/tarifas/calcular").hasRole("SYSTEM")
.requestMatchers("/api/v1/tarifas/historico/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/actuator/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.decoder(jwtDecoder())))
.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri("https://auth.banco.com/.well-known/jwks.json")
.cache(Duration.ofMinutes(5))
.build();
}
}
@Configuration
@EnableCaching
@EnableScheduling
@EnableAsync
public class TarifaApplicationConfig {
@Bean
@Primary
public CacheManager cacheManager() {
var cacheManager = new ConcurrentMapCacheManager(
"tabela-tarifas",
"regras-vigentes",
"promocoes-ativas"
);
cacheManager.setAllowNullValues(false);
return cacheManager;
}
@Bean
public TaskExecutor taskExecutor() {
var executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("tarifa-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Bean
public Resilience4JConfigurationProperties resilience4JProperties() {
return new Resilience4JConfigurationProperties();
}
}
4. Observability e Monitoring
@Component
public class TarifaMetrics {
private final MeterRegistry meterRegistry;
private final Counter calculosRealizados;
private final Counter isencoesConcedidas;
private final Timer tempoCalculoTarifa;
private final Gauge receitaTotalDia;
public TarifaMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.calculosRealizados = Counter.builder("tarifa.calculos.total")
.description("Total de cálculos de tarifa realizados")
.tag("component", "tarifa-service")
.register(meterRegistry);
this.isencoesConcedidas = Counter.builder("tarifa.isencoes.total")
.description("Total de isenções de tarifa concedidas")
.register(meterRegistry);
this.tempoCalculoTarifa = Timer.builder("tarifa.calculo.duration")
.description("Tempo de cálculo de tarifa")
.register(meterRegistry);
this.receitaTotalDia = Gauge.builder("tarifa.receita.total.dia")
.description("Receita total de tarifas do dia")
.register(meterRegistry, this, TarifaMetrics::calcularReceitaDia);
}
public void incrementarCalculos(TipoOperacao operacao, TipoConta tipoConta) {
calculosRealizados.increment(
Tags.of(
"operacao", operacao.name(),
"tipo_conta", tipoConta.name()
)
);
}
public void incrementarIsencao(String motivoIsencao) {
isencoesConcedidas.increment(Tags.of("motivo", motivoIsencao));
}
public Timer.Sample iniciarTimer() {
return Timer.start(meterRegistry);
}
public void finalizarTimer(Timer.Sample sample, String operacao, boolean sucesso) {
sample.stop(Timer.builder("tarifa.calculo.duration")
.tag("operacao", operacao)
.tag("sucesso", String.valueOf(sucesso))
.register(meterRegistry));
}
private double calcularReceitaDia() {
// Implementação para calcular receita do dia atual
return tarifaRepository.calcularReceitaDia(LocalDate.now())
.doubleValue();
}
}
@RestController
@RequestMapping("/actuator/custom")
public class TarifaHealthController {
private final TarifaService tarifaService;
private final DataSource dataSource;
@GetMapping("/health/tarifa")
public ResponseEntity<Map<String, Object>> healthCheck() {
var health = new HashMap<String, Object>();
try {
// Testa cálculo básico
var testRequest = new TarifaCalculationRequest(
TipoOperacao.PIX,
new BigDecimal("100"),
TipoConta.CORRENTE,
"test-client",
LocalDateTime.now()
);
var resultado = tarifaService.simularTarifa(testRequest);
health.put("status", "UP");
health.put("calculoTeste", "OK");
health.put("tempoResposta", "< 100ms");
} catch (Exception e) {
health.put("status", "DOWN");
health.put("erro", e.getMessage());
}
// Testa conectividade com banco
try (var connection = dataSource.getConnection()) {
health.put("database", "UP");
} catch (SQLException e) {
health.put("database", "DOWN");
health.put("databaseError", e.getMessage());
}
return ResponseEntity.ok(health);
}
}
Performance e Otimizações
1. Caching Inteligente
@Service
public class TarifaCacheService {
private final RedisTemplate<String, Object> redisTemplate;
private static final String CACHE_PREFIX = "tarifa:";
private static final Duration CACHE_TTL = Duration.ofHours(2);
@Cacheable(value = "tarifa-calculo",
key = "#request.operacao() + ':' + #request.tipoConta() + ':' + #request.valor()")
public TarifaCalculationResult calcularComCache(TarifaCalculationRequest request) {
// Implementação do cálculo
return calculationEngine.calcular(request);
}
@CacheEvict(value = "tarifa-calculo", allEntries = true)
@Scheduled(fixedRate = 3600000) // A cada hora
public void limparCacheAntico() {
log.info("Cache de tarifas limpo automaticamente");
}
// Cache warming para operações mais comuns
@EventListener(ApplicationReadyEvent.class)
public void preaquecerCache() {
log.info("Iniciando pré-aquecimento do cache de tarifas");
var operacoesComuns = List.of(TipoOperacao.PIX, TipoOperacao.TED);
var tiposContaComuns = List.of(TipoConta.CORRENTE, TipoConta.PREMIUM);
var valoresComuns = List.of(
new BigDecimal("100"),
new BigDecimal("1000"),
new BigDecimal("5000")
);
operacoesComuns.parallelStream().forEach(operacao -> {
tiposContaComuns.forEach(tipoConta -> {
valoresComuns.forEach(valor -> {
try {
var request = new TarifaCalculationRequest(
operacao, valor, tipoConta, "cache-warming", LocalDateTime.now()
);
calcularComCache(request);
} catch (Exception e) {
log.warn("Erro no pré-aquecimento: {}", e.getMessage());
}
});
});
});
log.info("Pré-aquecimento do cache concluído");
}
}
2. Processamento Assíncrono
@Service
public class TarifaAsyncService {
@Async("tarifaTaskExecutor")
@Retryable(maxAttempts = 3)
public CompletableFuture<Void> processarCobrancaAssincrona(List<TarifaCobrancaRequest> requests) {
var inicio = System.currentTimeMillis();
try {
// Processa em lotes para otimizar performance
var lotes = Lists.partition(requests, 100);
var futures = lotes.stream()
.map(this::processarLote)
.collect(Collectors.toList());
// Aguarda todos os lotes
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
var duracao = System.currentTimeMillis() - inicio;
log.info("Processamento assíncrono concluído em {}ms para {} itens",
duracao, requests.size());
return CompletableFuture.completedFuture(null);
} catch (Exception e) {
log.error("Erro no processamento assíncrono: {}", e.getMessage());
return CompletableFuture.failedFuture(e);
}
}
private CompletableFuture<Void> processarLote(List<TarifaCobrancaRequest> lote) {
return CompletableFuture.runAsync(() -> {
lote.parallelStream().forEach(request -> {
try {
tarifaService.cobrarTarifa(request);
} catch (Exception e) {
log.error("Erro ao cobrar tarifa {}: {}", request.getId(), e.getMessage());
// Enviar para dead letter queue
deadLetterService.enviar(request, e);
}
});
});
}
}
Testes Modernos
1. Testes Unitários com JUnit 5
@ExtendWith(MockitoExtension.class)
class TarifaServiceTest {
@Mock
private TarifaRepository tarifaRepository;
@Mock
private ClienteService clienteService;
@InjectMocks
private TarifaService tarifaService;
@Test
@DisplayName("Deve calcular tarifa PIX corretamente para valor baixo")
void deveCalcularTarifaPixValorBaixo() {
// Given
var request = new TarifaCalculationRequest(
TipoOperacao.PIX,
new BigDecimal("500"),
TipoConta.CORRENTE,
"cliente-123",
LocalDateTime.now()
);
var cliente = Cliente.builder()
.id("cliente-123")
.tipoConta(TipoConta.CORRENTE)
.build();
when(clienteService.findById("cliente-123")).thenReturn(cliente);
// When
var resultado = tarifaService.calcularTarifa(request, "cliente-123");
// Then
assertThat(resultado.valor()).isEqualByComparingTo(BigDecimal.ZERO);
assertThat(resultado.isencaoAplicada()).isTrue();
assertThat(resultado.regraAplicada()).contains("PIX_VALOR_BAIXO");
}
@ParameterizedTest
@MethodSource("provideTarifaTestCases")
@DisplayName("Deve calcular tarifas corretamente para diferentes cenários")
void deveCalcularTarifasCorretamente(TarifaTestCase testCase) {
// Given
when(clienteService.findById(testCase.clienteId())).thenReturn(testCase.cliente());
when(tarifaRepository.findRegrasVigentes(any(), any(), any())).thenReturn(testCase.regras());
// When
var resultado = tarifaService.calcularTarifa(testCase.request(), testCase.clienteId());
// Then
assertThat(resultado.valor()).isEqualByComparingTo(testCase.valorEsperado());
}
private static Stream<TarifaTestCase> provideTarifaTestCases() {
return Stream.of(
// PIX até 1000 - isento
TarifaTestCase.builder()
.request(new TarifaCalculationRequest(TipoOperacao.PIX, new BigDecimal("800"), TipoConta.CORRENTE, "c1", LocalDateTime.now()))
.cliente(Cliente.builder().tipoConta(TipoConta.CORRENTE).build())
.valorEsperado(BigDecimal.ZERO)
.build(),
// TED conta premium - isento
TarifaTestCase.builder()
.request(new TarifaCalculationRequest(TipoOperacao.TED, new BigDecimal("5000"), TipoConta.PREMIUM, "c2", LocalDateTime.now()))
.cliente(Cliente.builder().tipoConta(TipoConta.PREMIUM).build())
.valorEsperado(BigDecimal.ZERO)
.build()
);
}
}
2. Testes de Integração
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class TarifaIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("tarifas_test")
.withUsername("test")
.withPassword("test");
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private TarifaRepository tarifaRepository;
@Test
@Sql("/data/setup-tarifas.sql")
void deveCalcularTarifaViaAPI() {
// Given
var request = new TarifaCalculationRequest(
TipoOperacao.PIX,
new BigDecimal("1500"),
TipoConta.CORRENTE,
"cliente-test",
LocalDateTime.now()
);
var headers = new HttpHeaders();
headers.set("X-Cliente-ID", "cliente-test");
headers.set("Authorization", "Bearer " + gerarTokenTeste());
var entity = new HttpEntity<>(request, headers);
// When
var response = restTemplate.postForEntity(
"/api/v1/tarifas/calcular",
entity,
TarifaResponse.class
);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getValor()).isEqualByComparingTo(new BigDecimal("1.50"));
}
}
Conclusion
Java 11+ e Spring Boot transformam o desenvolvimento de sistemas de tarifas bancárias, oferecendo:
Principais benefícios:
- Performance: HTTP Client nativo, otimizações de JVM, GC melhorado
- Produtividade: Records, pattern matching, text blocks, var
- Escalabilidade: Spring Boot auto-configuration, caching, async processing
- Observabilidade: Métricas automáticas, health checks, tracing distribuído
- Segurança: OAuth2, JWT, method security para APIs sensíveis
Próximos passos:
No próximo artigo, exploraremos Microserviços de Tarifas com Docker e Kubernetes, incluindo deployment automatizado, service mesh e monitoring distribuído para alta disponibilidade.