Capítulo 31 — Backend: Java e Spring Boot
Este capítulo descreve a arquitetura, os padrões, a estrutura de código e as tecnologias do backend da Plataforma de Relacionamento Digital com o Cidadão. O backend é implementado em Java com Spring Boot, seguindo padrõe…
31.1 Objetivo do Capítulo
Este capítulo descreve a arquitetura, os padrões, a estrutura de código e as tecnologias do backend da Plataforma de Relacionamento Digital com o Cidadão. O backend é implementado em Java com Spring Boot, seguindo padrões de Arquitetura Hexagonal (Ports & Adapters) e Domain-Driven Design (DDD) para garantir manutenibilidade, escalabilidade e conformidade com os requisitos de segurança, LGPD e observabilidade.
O capítulo responde: como o código está organizado; quais são os padrões internos aplicados em cada serviço; como a identidade e o contexto de tenant fluem pela lógica de negócio; como o serviço se integra com infraestrutura (banco, cache, fila, busca); como testes são estruturados; e como a plataforma escala horizontalmente sem comprometer a consistência de dados.
31.2 Stack Técnico Confirmado
31.2.1 Linguagem e Framework
Java (versão LTS vigente) — linguagem compilada, fortemente tipada, com ecosystem maduro de bibliotecas, frameworks e ferramentas.
Spring Boot — framework que acelera o desenvolvimento de aplicações Java ao fornecer configuração automática, convenções sensatas e integração com componentes do ecosystem Spring (Data, Security, Cloud, etc.). A escolha de Spring Boot é registrada em ADR-001 e justifica-se por: maturidade comprovada em ambientes de produção crítica; comunidade ativa; compatibilidade com padrões modernos (REST, reatividade, containers); suporte a cloud-native patterns.
31.2.2 Bibliotecas Principais
spring-boot-starter-web REST controllers, embedded Tomcat
spring-boot-starter-security Autenticação, autorização
spring-boot-starter-data-jpa ORM, Hibernate, repositórios
spring-boot-starter-data-redis Cache distribuído
spring-boot-starter-amqp Mensageria RabbitMQ
spring-boot-starter-validation Validação de entrada
spring-boot-starter-actuator Health checks, métricas
micrometer-registry-prometheus Prometheus metrics
spring-cloud-starter-sleuth Tracing distribuído
spring-cloud-starter-zipkin Zipkin (quando configurado)
jackson Serialização JSON
lombok Redução de boilerplate
mapstruct Mapeamento entre DTOs e entidades
vavr / eclipse-collections Estruturas de dados funcionais
openapi-starter Documentação de API (Swagger)
testcontainers Testes de integração
junit5 / mockito Testes unitários
31.2.3 Versioning e Compatibilidade
Não há versões pinadas de forma restrita neste documento (conforme diretriz do projeto). As dependências seguem as recomendações de compatibilidade do Spring Boot e são gerenciadas via Maven ou Gradle com dependencyManagement para evitar conflitos transitivos.
31.3 Princípios Arquiteturais do Backend
31.3.1 Hexagonal (Ports & Adapters)
A Arquitetura Hexagonal separa a lógica de negócio da infraestrutura por meio de portas (interfaces) e adaptadores (implementações). O núcleo da aplicação — o domínio — não depende de bibliotecas externas, bancos de dados ou frameworks web. As dependências apontam sempre para dentro: infraestrutura depende do domínio, nunca o contrário.
┌─────────────────────────────────────────────────┐
│ Web (Spring MVC) │
│ ├─ @RestController │
│ └─ @RequestMapping │
├─────────────────────────────────────────────────┤
│ Application Layer (DTOs, Mappers) │
│ ├─ Commands, Queries │
│ └─ Application Services │
├─────────────────────────────────────────────────┤
│ Domain Layer (Entities, ValueObjects, Rules) │
│ ├─ Aggregates │
│ ├─ Domain Services │
│ └─ Ports (interfaces) │
├─────────────────────────────────────────────────┤
│ Infrastructure (Adapters) │
│ ├─ Spring Data Repositories (JPA) │
│ ├─ Cache (Redis Adapter) │
│ ├─ Messaging (RabbitMQ Adapter) │
│ ├─ Search (ElasticSearch Adapter) │
│ └─ External Integrations (HTTP Clients) │
└─────────────────────────────────────────────────┘
31.3.2 Domain-Driven Design (DDD)
DDD estrutura o código em torno de conceitos de domínio — Agregados, Entidades, Value Objects, Domain Services, Repositories — em vez de estruturas técnicas (Controllers, DAOs, Utilities). O benefício é que a lógica de negócio é explícita e fácil de testar isoladamente.
Agregados representam raízes de consistência. Exemplo: um Agregado Solicitação contém Tarefa, Documento, Histórico de Status como partes internas. Operações em uma Solicitação garantem consistência interna antes de ser persistida.
Value Objects representam conceitos sem identidade própria (ex: Email, Status, Coordenadas). São imutáveis e comparáveis por valor.
Domain Services encapsulam lógica que não pertence a uma Entidade específica (ex: CalculadoraDeSLA, ValidadorDeFila).
31.3.3 Inversão de Controle e Injeção de Dependências
Spring gerencia o ciclo de vida de beans: criação, inicialização, injeção e destruição. A configuração ocorre por anotações (@Component, @Service, @Repository, @Configuration) ou por métodos @Bean em classes de configuração. A injeção é feita por construtor — nunca por setter ou campo, para evitar dependências ocultas e facilitar testes.
@Service
public class CriarSolicitacaoService {
private final SolicitacaoRepository repo;
private final SolicitacaoValidator validator;
private final SolicitacaoPublisher publisher;
// Construtor com injeção explícita
public CriarSolicitacaoService(
SolicitacaoRepository repo,
SolicitacaoValidator validator,
SolicitacaoPublisher publisher
) {
this.repo = repo;
this.validator = validator;
this.publisher = publisher;
}
}
31.4 Estrutura de Pacotes e Organização do Código
31.4.1 Organização por Domínio (Feature Packages)
Cada microserviço é organizado por domínio funcional, não por camada técnica. Esta abordagem melhora a coesão e facilita a evolução independente:
br.gov.mg.cidadao
├── solicitacoes/
│ ├── domain/
│ │ ├── Solicitacao.java (Agregado raiz)
│ │ ├── Tarefa.java (Entidade)
│ │ ├── Status.java (Value Object)
│ │ ├── SolicitacaoRepository.java (Porta)
│ │ ├── SolicitacaoValidator.java (Domain Service)
│ │ └── SolicitacaoPublisher.java (Porta)
│ ├── application/
│ │ ├── CriarSolicitacaoCommand.java (Command)
│ │ ├── CriarSolicitacaoUseCase.java (Application Service)
│ │ ├── SolicitacaoDTO.java (DTO)
│ │ └── SolicitacaoDTOMapper.java (Mapper)
│ ├── infrastructure/
│ │ ├── persistence/
│ │ │ ├── SolicitacaoJpaRepository.java (Spring Data)
│ │ │ ├── SolicitacaoJpaEntity.java (JPA Entity)
│ │ │ └── SolicitacaoRepositoryAdapter.java (Implementação da porta)
│ │ ├── messaging/
│ │ │ └── SolicitacaoPublisherRabbitMQ.java (Adaptador)
│ │ └── search/
│ │ └── SolicitacaoSearchAdapter.java (ElasticSearch)
│ └── web/
│ └── SolicitacoesController.java (REST endpoints)
├── cidadao/
│ ├── domain/
│ ├── application/
│ ├── infrastructure/
│ └── web/
├── catálogo/
│ └── ...
└── config/
├── SecurityConfig.java
├── CacheConfig.java
├── MessagingConfig.java
└── OpenApiConfig.java
Esta estrutura permite que um desenvolvedor entenda o domínio solicitacoes navegando um único diretório, sem espalhamento entre dezenas de pacotes.
31.4.2 Configuração por Profiles
Spring suporta múltiplos profiles de ambiente (dev, staging, prod) com configurações separadas:
# application.yml
spring:
profiles:
active: ${SPRING_PROFILE:dev}
---
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:postgresql://localhost:5432/cidadao_dev
hikari:
maximum-pool-size: 5
---
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:postgresql://postgres-prod:5432/cidadao
hikari:
maximum-pool-size: 20
jpa:
show-sql: false
properties:
hibernate.format_sql: false
Variáveis de ambiente sempre têm precedência sobre arquivos YAML, permitindo configuração containerizada sem recompilação.
31.5 Gerenciamento de Identidade e Contexto
31.5.1 TenantContext como ThreadLocal
Toda requisição HTTP entra no sistema com um JWT validado pelo API Gateway. Um filtro Spring extrai o JWT e estabelece o TenantContext — informação que identifica qual tenant, qual usuário, quais permissões:
@Component
public class TenantContextFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String token = extractToken(request);
if (token != null) {
Claims claims = jwtParser.parseClaimsJws(token).getBody();
String tenantId = claims.get("tenant_id", String.class);
String userId = claims.getSubject();
List<String> permissions = claims.get("permissions", List.class);
TenantContextHolder.setContext(new TenantContext(
tenantId, userId, permissions
));
}
try {
filterChain.doFilter(request, response);
} finally {
TenantContextHolder.clear();
}
}
}
O TenantContext é armazenado em ThreadLocal para estar disponível em qualquer ponto do código sem necessidade de passar como parâmetro. Após a requisição, o contexto é limpo para evitar vazamento entre requisições em pools de threads.
31.5.2 Propagação em Chamadas Assíncronas
Quando um serviço publica um evento em RabbitMQ ou executa uma tarefa assíncrona, o contexto de tenant é serializado e enviado junto. O consumidor (em thread diferente) restaura o contexto antes de processar:
public class SolicitacaoPublishedEvent {
private final String tenantId;
private final String solicitacaoId;
private final LocalDateTime timestamp;
// serializa tenant para que consumidor saiba em qual contexto processar
}
31.6 Camada de Persistência com JPA e Hibernate
31.6.1 Entidades JPA e Mapeamento
Entidades JPA mapeiam tabelas de banco de dados. A convenção é que entidades JPA resialam em infrastructure/persistence, não no domínio, para manter o núcleo de negócio independente do ORM:
@Entity
@Table(name = "solicitacoes", schema = "cidadao_core")
public class SolicitacaoJpaEntity {
@Id
private String id;
@Column(name = "tenant_id", nullable = false)
private String tenantId;
@Column(name = "cidadao_id", nullable = false)
private String cidadaoId;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private SolicitacaoStatus status;
@Column(name = "criada_em", nullable = false, updatable = false)
private LocalDateTime criadaEm;
@Column(name = "atualizada_em")
private LocalDateTime atualizadaEm;
@Version
@Column(name = "versao")
private Long versao; // Optimistic locking
}
Optimistic Locking (coluna @Version) previne conflitos de escrita em cenários de alta concorrência. Se duas threads tentam atualizar a mesma entidade, uma falha com OptimisticLockingFailureException — o chamador retenta.
31.6.2 Specifications para Queries Dinâmicas
Spring Data JPA oferece Specification para construir predicados SQL dinamicamente:
public class SolicitacaoSpecs {
public static Specification<SolicitacaoJpaEntity> comTenant(String tenantId) {
return (root, query, cb) -> cb.equal(root.get("tenantId"), tenantId);
}
public static Specification<SolicitacaoJpaEntity> comStatus(SolicitacaoStatus status) {
return (root, query, cb) -> cb.equal(root.get("status"), status);
}
}
// Uso
List<SolicitacaoJpaEntity> solicitacoes = repository.findAll(
Specification.where(SolicitacaoSpecs.comTenant(TenantContextHolder.get().getTenantId()))
.and(SolicitacaoSpecs.comStatus(PENDENTE))
);
O padrão Specification garante que filtros de tenant são sempre aplicados — não há chance de uma query "escapar" do isolamento.
31.6.3 Soft Delete e Data Retention
Registros nunca são fisicamente excluídos. Uma coluna deletado_em marca exclusão lógica. Ao implementar @SQLDelete e @Where, Hibernate adiciona automaticamente as cláusulas:
@Entity
@SQLDelete(sql = "UPDATE solicitacoes SET deletado_em = NOW() WHERE id = ?")
@Where(clause = "deletado_em IS NULL")
public class SolicitacaoJpaEntity {
@Column(name = "deletado_em")
private LocalDateTime deletadoEm;
}
Quando selecionada, a query inclui WHERE deletado_em IS NULL automaticamente. Isso garante conformidade com LGPD — dados nunca são realmente descartados; podem ser recuperados se necessário.
31.7 Gestão de Transações
31.7.1 Transacional por Padrão
Métodos de serviço são anotados com @Transactional para indicar que devem executar dentro de uma transação:
@Service
@Transactional
public class CriarSolicitacaoUseCase {
public SolicitacaoId executar(CriarSolicitacaoCommand cmd) {
// Toda lógica ocorre em transação
// Se uma exceção lançar antes do fim, tudo é revertido
// Se completar, commit é automático
Solicitacao solicitacao = new Solicitacao(
cmd.cidadaoId(),
cmd.servicoId()
);
SolicitacaoId id = solicitacaoRepository.salvar(solicitacao);
solicitacaoPublisher.publicarCriada(solicitacao);
return id;
}
}
Se o solicitacaoPublisher.publicarCriada() falhar, a transação é revertida e nenhum registro é inserido.
31.7.2 Propagação de Transação e Read-Only
@Transactional(readOnly = true)
public class ListarSolicitacoesUseCase {
// Queries não adquirem locks de escrita
// Hibernate otimiza para leitura
public List<SolicitacaoDTO> executar(ListarSolicitacoesQuery qry) {
// ...
}
}
Métodos marcados como readOnly = true indicam ao Hibernate e ao banco de dados que locks de escrita são desnecessários — melhora performance em queries pesadas.
31.8 Cache Distribuído com Redis
31.8.1 Estratégia de Cache
O cache é aplicado em múltiplas dimensões:
Cache de Identidade: Usuários, perfis e permissões são consultados frequentemente. Um cache de leitura reduz latência e carga no IAM. A invalidação ocorre quando um perfil é alterado ou um usuário é desbloqueado.
Cache de Catálogo: Serviços publicados mudam raramente. São cached após o primeiro acesso.
Cache de Configuração: Políticas de tenant (timeouts, retenção, limites) são cached por tenant.
Cache de Consultas Analíticas: Agregações de dashboard são cached com TTL de minutos.
@Service
public class CatalogoService {
@Cacheable(
value = "catalogo:servicos",
key = "#tenantId + ':' + #servicoId",
unless = "#result == null"
)
public ServicoDTO obterServico(String tenantId, String servicoId) {
// Query ao banco — resultado é cacheado
return servicoRepository.findByIdAndTenantId(servicoId, tenantId)
.map(servicoMapper::toDomain)
.orElse(null);
}
@CacheEvict(
value = "catalogo:servicos",
key = "#tenantId + ':' + #servicoId"
)
public void atualizarServico(String tenantId, String servicoId, ServicoDTO dto) {
// Invalidação explícita do cache após atualização
}
}
31.8.2 Serialização e Namespace
Redis armazena objetos como strings JSON. A serialização é feita por Jackson. Cada tenant tem namespace separado:
@Configuration
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()
))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
));
return RedisCacheManager.create(factory);
}
}
O prefixo do tenant (catalogo:servicos:{tenantId}:{servicoId}) garante isolamento. Um administrador não consegue acessar dados cacheados de outro tenant mesmo tendo acesso direto a Redis.
31.8.3 Prevenção de Cache Stampede
Quando um item expira e centenas de requisições chegam simultaneamente, todas tentam refazer a consulta ao banco — spike de carga. A técnica de lock-and-refresh evita:
@Service
public class ConfiguracaoService {
public ConfiguracaoDTO obterConfiguracao(String tenantId) {
String cacheKey = "config:" + tenantId;
ConfiguracaoDTO cached = cacheStore.get(cacheKey);
if (cached != null && !estaProximoDeExpirar(cached)) {
return cached;
}
// Apenas uma thread executa refresh; outras esperam
synchronized (cacheKey.intern()) {
cached = cacheStore.get(cacheKey);
if (cached != null) return cached; // Outra thread já atualizou
ConfiguracaoDTO nova = carregarDoBanco(tenantId);
cacheStore.set(cacheKey, nova, TTL_MINUTES);
return nova;
}
}
}
31.9 Mensageria com RabbitMQ
31.9.1 Topologia de Exchanges e Filas
A plataforma define exchanges por domínio e filas por serviço consumidor:
Exchange: solicitacoes (topic)
├─ Topic: solicitacoes.criada
│ └─ Fila: solicitacoes:service:criada
│ └─ Fila: comunicacao:service:notificar-criada
├─ Topic: solicitacoes.aprovada
│ └─ Fila: solicitacoes:service:aprovada
│ └─ Fila: cidadao:service:atualizar-solicitacoes
└─ Topic: solicitacoes.rejeitada
└─ Fila: solicitacoes:service:rejeitada
Exchange: comunicacao (topic)
├─ Topic: email.enviar
└─ Topic: sms.enviar
A topologia permite que múltiplos serviços reagem ao mesmo evento (solicitação criada → notificar cidadão, calcular SLA, gerar documento).
31.9.2 Publishers e Subscribers
@Service
public class SolicitacaoPublisher {
private final RabbitTemplate template;
public void publicarCriada(Solicitacao solicitacao) {
SolicitacaoCriadaEvent event = new SolicitacaoCriadaEvent(
solicitacao.getId(),
solicitacao.getTenantId(),
solicitacao.getCidadaoId(),
LocalDateTime.now()
);
template.convertAndSend(
"solicitacoes", // exchange
"solicitacoes.criada", // routing key
event
);
}
}
@Service
public class NotificacaoSolicitacaoListener {
@RabbitListener(queues = "comunicacao:service:notificar-criada")
public void aoSolicitacaoCriada(SolicitacaoCriadaEvent event) {
TenantContextHolder.setContext(new TenantContext(event.getTenantId(), null, List.of()));
try {
// Enviar notificação ao cidadão
notificacaoService.notificarCriacao(event.getCidadaoId(), event.getSolicitacaoId());
} finally {
TenantContextHolder.clear();
}
}
}
O tenant é restaurado do evento para garantir que a notificação é processada no contexto correto.
31.9.3 Dead Letter Queue (DLQ) e Retry
Mensagens que falham são automaticamente enviadas à DLQ após N tentativas:
@Configuration
public class RabbitConfig {
@Bean
public Queue solicitacoesCriadaQueue() {
return QueueBuilder.durable("solicitacoes:service:criada")
.deadLetterExchange("dlx")
.deadLetterRoutingKey("solicitacoes.criada.dlq")
.ttl(Duration.ofMinutes(5)) // TTL da mensagem
.maxLength(10000) // Max mensagens na fila
.build();
}
@Bean
public Queue dlq() {
return QueueBuilder.durable("solicitacoes:service:criada:dlq")
.ttl(Duration.ofDays(7)) // Manter DLQ por 7 dias
.build();
}
}
Quando uma mensagem está na DLQ, um alerta é acionado para que o time de operações investigue.
31.10 Busca Semântica com ElasticSearch
31.10.1 Indexação de Documentos
Solicitações, documentos e interações são indexados em ElasticSearch para permitir busca full-text e filtros multidimensionais:
@Document(indexName = "solicitacoes-#{@tenantContextHolder.get().getTenantId()}")
public class SolicitacaoSearch {
@Id
private String id;
@Field(type = FieldType.Keyword)
private String tenantId;
@Field(type = FieldType.Text)
private String titulo;
@Field(type = FieldType.Text, analyzer = "portuguese")
private String descricao;
@Field(type = FieldType.Keyword)
private SolicitacaoStatus status;
@Field(type = FieldType.Date)
private LocalDateTime criadaEm;
@Field(type = FieldType.Dense_Vector)
private float[] embedding; // Vetor de embeddings para RAG
}
Cada tenant tem índice separado (solicitacoes-{tenantId}) para isolar buscas.
31.10.2 Sincronização de Índice
Quando uma solicitação é criada ou atualizada no banco de dados, é publicado um evento que dispara indexação assíncrona:
@Service
public class SolicitacaoIndexService {
@RabbitListener(queues = "search:service:index-solicitacao")
public void indexarSolicitacao(SolicitacaoAtualizadaEvent event) {
Solicitacao solicitacao = solicitacaoRepository.findById(event.getSolicitacaoId());
SolicitacaoSearch searchDoc = mapper.toSearchDocument(solicitacao);
// Gerar embedding via LLM Gateway
searchDoc.setEmbedding(embeddingService.gerar(solicitacao.getDescricao()));
repository.save(searchDoc);
}
}
Embeddings são calculados via LLM Gateway para permitir buscas semânticas ("solicitações sobre educação" encontra registros com conteúdo relacionado mesmo sem palavra exata).
31.11 Chamadas a Serviços Externos
31.11.1 HTTP Client com RestClient
Spring fornece RestClient (em versões recentes) ou RestTemplate para chamadas HTTP. A convenção é usar RestClient para código limpo:
@Configuration
public class ExternalServicesConfig {
@Bean
public RestClient govBrClient(RestClient.Builder builder) {
return builder
.baseUrl("https://api.gov.br")
.defaultHeader("Authorization", "Bearer " + System.getenv("GOVBR_API_KEY"))
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.build();
}
@Bean
public RestClient seiMgClient(RestClient.Builder builder) {
return builder
.baseUrl("https://sei-mg.prodemge.gov.br/api")
.build();
}
}
@Service
public class GovBrIntegrationService {
private final RestClient govBrClient;
public PessoaDTO obterDadosPessoa(String cpf) {
try {
return govBrClient.get()
.uri("/pessoas/{cpf}", cpf)
.retrieve()
.body(PessoaDTO.class);
} catch (HttpClientErrorException.NotFound e) {
throw new PessoaNaoEncontradaException(cpf);
} catch (HttpServerErrorException e) {
throw new GovBrIndisponibilException(e);
}
}
}
Exceções específicas são capturadas e mapeadas para erros de domínio — o chamador não precisa conhecer detalhes de HTTP.
31.11.2 Resiliência com Circuit Breaker
O padrão Circuit Breaker evita cascata de falhas. Se uma integração externa falha repetidamente, o circuito abre e as requisições são rejeitadas imediatamente sem tentar:
@Configuration
public class CircuitBreakerConfig {
@Bean
public CircuitBreaker govBrCircuitBreaker() {
return CircuitBreaker.ofDefaults("govbr")
.builder()
.failureThreshold(5) // Abre após 5 falhas
.slowCallDurationThreshold(Duration.ofSeconds(2))
.slowCallRateThreshold(50) // Abre se 50% das chamadas são lentas
.waitDurationInOpenState(Duration.ofMinutes(1))
.build();
}
}
@Service
public class GovBrIntegrationService {
@CircuitBreaker(name = "govbr")
public PessoaDTO obterDadosPessoa(String cpf) {
return govBrClient.get()
.uri("/pessoas/{cpf}", cpf)
.retrieve()
.body(PessoaDTO.class);
}
}
31.12 Camada de Aplicação (Use Cases)
31.12.1 Commands e Queries
A separação entre escrita (Commands) e leitura (Queries) — CQRS simplificado — melhora clareza:
// Command (muta estado)
public record CriarSolicitacaoCommand(
String servicoId,
String cidadaoId,
Map<String, String> respostas,
List<String> documentoIds
) {}
// Query (apenas lê)
public record ListarSolicitacoesQuery(
SolicitacaoStatus status,
LocalDate dataDe,
LocalDate dataAte,
int pagina,
int tamanho
) {}
@Service
public class SolicitacaoUseCases {
public SolicitacaoDTO criar(CriarSolicitacaoCommand cmd) {
// Validar, criar, persistir, publicar evento
}
public Page<SolicitacaoDTO> listar(ListarSolicitacoesQuery qry) {
// Consultar índice ElasticSearch ou banco
// Nunca modifica estado
}
}
Commands e Queries são objetos imutáveis (record), facilitando serialização e logging.
31.12.2 Validação em Múltiplas Camadas
@Service
public class CriarSolicitacaoUseCase {
@Transactional
public SolicitacaoDTO executar(CriarSolicitacaoCommand cmd) {
// Camada 1: Validação de entrada (não-nulo, formato)
CriarSolicitacaoValidator.validar(cmd);
// Camada 2: Regras de domínio
Servico servico = servicoRepository.findById(cmd.servicoId())
.orElseThrow(ServicoNaoEncontradoException::new);
if (servico.temFormularioObrigatorio() && cmd.respostas().isEmpty()) {
throw new FormularioObrigatorioException();
}
// Camada 3: Autorização
String cidadaoId = TenantContextHolder.get().getCidadaoId();
if (!cidadaoId.equals(cmd.cidadaoId())) {
throw new AcessoNegadoException("Não pode criar solicitação para outro cidadão");
}
// Camada 4: Persistência e propagação
Solicitacao solicitacao = new Solicitacao(cmd.servicoId(), cidadaoId, cmd.respostas());
SolicitacaoId id = solicitacaoRepository.salvar(solicitacao);
solicitacaoPublisher.publicarCriada(solicitacao);
return mapper.toDTO(solicitacao);
}
}
Validações ocorrem em ordem de custoputacional: sintaxe → regras simples → consultas ao banco.
31.13 Testes e Cobertura
31.13.1 Teste Unitário de Domínio
Testes do núcleo de negócio (entidades, value objects, domain services) não requerem Spring. São testes PURO:
class SolicitacaoTest {
@Test
void deveCriarSolicitacaoComStatusInicial() {
// Arrange
String servicoId = "srv-123";
String cidadaoId = "cid-456";
Map<String, String> respostas = Map.of("campo1", "valor1");
// Act
Solicitacao sol = new Solicitacao(servicoId, cidadaoId, respostas);
// Assert
assertThat(sol.getId()).isNotNull();
assertThat(sol.getStatus()).isEqualTo(NOVA);
assertThat(sol.getCriadaEm()).isNotNull();
}
@Test
void naoDeveAprovarSeStatusNaoEhPendente() {
Solicitacao sol = new Solicitacao("srv", "cid", Map.of());
sol.marcarComoConcluida(); // Muda status para CONCLUIDA
assertThatThrownBy(() -> sol.aprovar("analista"))
.isInstanceOf(SolicitacaoJaConcluidaException.class);
}
}
Testes de domínio são rápidos e determinísticos — nenhuma dependência externa.
31.13.2 Teste de Integração com Testcontainers
Testes que verificam integração com banco de dados, cache, filas usam Testcontainers para isolar dependências:
@SpringBootTest
@Testcontainers
class SolicitacaoRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>()
.withDatabaseName("cidadao_test")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void setProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Test
void deveEncontrarSolicitacoesPorTenant() {
// Arrange
SolicitacaoJpaEntity entity = new SolicitacaoJpaEntity();
entity.setTenantId("tenant-1");
repository.save(entity);
// Act
List<SolicitacaoJpaEntity> result = repository.findAllByTenantId("tenant-1");
// Assert
assertThat(result).hasSize(1);
assertThat(result.get(0).getTenantId()).isEqualTo("tenant-1");
}
}
Testcontainers gerencia o ciclo de vida de containers Docker — não é preciso configurar manualmente.
31.14 Observabilidade Integrada
31.14.1 Logging Estruturado
Logs não são strings livres. Toda entrada é JSON estruturado com campos obrigatórios:
@Configuration
public class LoggingConfig {
@Bean
public JsonProvider jsonProvider() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
return new JsonProvider(mapper);
}
}
@Service
public class SolicitacaoService {
private static final Logger logger = LoggerFactory.getLogger(SolicitacaoService.class);
public void criar(CriarSolicitacaoCommand cmd) {
long inicio = System.currentTimeMillis();
String correlationId = MDC.get("correlationId");
String tenantId = TenantContextHolder.get().getTenantId();
try {
logger.info("Iniciando criação de solicitação",
new StructuredArguments.map("evento", "SOLICITACAO_CRIACAO_INICIADA")
.put("servicoId", cmd.servicoId())
.put("tenantId", tenantId)
.put("correlationId", correlationId));
Solicitacao sol = criar_internamente(cmd);
long duracao = System.currentTimeMillis() - inicio;
logger.info("Solicitação criada com sucesso",
new StructuredArguments.map("evento", "SOLICITACAO_CRIADA")
.put("solicitacaoId", sol.getId())
.put("durationMs", duracao)
.put("tenantId", tenantId));
} catch (Exception e) {
logger.error("Erro ao criar solicitação",
new StructuredArguments.map("evento", "SOLICITACAO_CRIACAO_ERRO")
.put("servicoId", cmd.servicoId())
.put("erro", e.getClass().getSimpleName())
.put("mensagem", e.getMessage()),
e);
throw new SolicitacaoCriacaoException(e);
}
}
}
Logs estruturados permitem agregação eficiente em ferramentas como Elasticsearch, Loki ou CloudWatch. Campos obrigatórios (evento, correlationId, tenantId) facilitam rastreamento.
31.14.2 Métricas com Micrometer
Métricas de negócio e infraestrutura são coletadas automaticamente:
@Service
public class SolicitacaoMetricsService {
private final MeterRegistry meterRegistry;
private final Counter solicitacoesCriadas;
private final Timer tempoProcessamento;
public SolicitacaoMetricsService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.solicitacoesCriadas = Counter.builder("solicitacoes_criadas_total")
.tag("tipo", "negocio")
.register(meterRegistry);
this.tempoProcessamento = Timer.builder("solicitacao_processamento_duracao")
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry);
}
public void registrarCriacao(String tenantId, String servicoId) {
solicitacoesCriadas
.tag("tenantId", tenantId)
.tag("servicoId", servicoId)
.increment();
}
public void registrarTempoProcessamento(long durationMs) {
tempoProcessamento.record(Duration.ofMillis(durationMs));
}
}
Métricas são expostas via /actuator/prometheus em formato Prometheus, ingeridas por Grafana.
31.14.3 Tracing Distribuído com OpenTelemetry
OpenTelemetry automaticamente rastreia requisições através de múltiplos serviços:
@Configuration
public class TracingConfig {
@Bean
public OpenTelemetry openTelemetry(
SdkTracerProvider tracerProvider,
SdkMeterProvider meterProvider
) {
return OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.setMeterProvider(meterProvider)
.buildAndRegisterGlobal();
}
}
@Service
public class SolicitacaoIntegrationService {
private final Tracer tracer;
public SolicitacaoIntegrationService() {
this.tracer = OpenTelemetry.getGlobalTracer(SolicitacaoIntegrationService.class.getName());
}
public void publicarEmSEI(SolicitacaoId id) {
Span span = tracer.spanBuilder("publicar_em_sei")
.setAttribute("solicitacaoId", id.value())
.setAttribute("servico", "SEI!MG")
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Chamada a SEI!MG — trace passa automaticamente via headers
seiIntegration.registrarDocumento(id);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR);
throw e;
} finally {
span.end();
}
}
}
O trace de uma requisição do cidadão passa por API Gateway → SolicitacaoService → SEI!MG Integration → SEI!MG, com correlação automática entre spans.
31.15 Segurança na Camada de Aplicação
31.15.1 Validação de Entrada
Toda entrada de usuário é validada com Jakarta Bean Validation:
public record CriarSolicitacaoCommand(
@NotBlank(message = "servicoId é obrigatório")
String servicoId,
@NotBlank(message = "cidadaoId é obrigatório")
String cidadaoId,
@NotNull(message = "respostas é obrigatório")
@Size(min = 1, message = "pelo menos uma resposta é obrigatória")
Map<String, @NotBlank String> respostas,
@NotNull
@Size(max = 20, message = "máximo 20 documentos")
List<@NotBlank String> documentoIds
) {}
@RestController
@RequestMapping("/api/solicitacoes")
public class SolicitacaoController {
@PostMapping
public ResponseEntity<SolicitacaoDTO> criar(
@Valid @RequestBody CriarSolicitacaoCommand cmd
) {
// cmd já foi validado pelo Spring
SolicitacaoDTO result = useCase.criar(cmd);
return ResponseEntity.status(HttpStatus.CREATED).body(result);
}
}
Se validação falha, Spring retorna HTTP 400 com detalhe dos erros. Nenhuma requisição inválida chega à lógica de negócio.
31.15.2 Proteção contra CSRF e XSS
CSRF é mitigado via tokens sincronizados. XSS é evitado codificando saídas no frontend (React) ou usando Content Security Policy:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.headers(headers -> headers
.contentSecurityPolicy("default-src 'self'; script-src 'self' 'nonce-{random}'")
.and()
.xssProtection()
)
.build();
return http.build();
}
}
31.16 Escalabilidade Horizontal
31.16.1 Stateless Design
Cada instância do serviço é interchangeável. Não há estado local persistente:
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // Nova instância por requisição
public class SolicitacaoService {
// Nenhum field de instância (stateful)
// Tudo injetado via constructor
private final SolicitacaoRepository repository;
private final SolicitacaoPublisher publisher;
private final RedisTemplate redisTemplate;
public SolicitacaoService(
SolicitacaoRepository repository,
SolicitacaoPublisher publisher,
RedisTemplate redisTemplate
) {
this.repository = repository;
this.publisher = publisher;
this.redisTemplate = redisTemplate;
}
@Transactional
public SolicitacaoDTO criar(CriarSolicitacaoCommand cmd) {
// Todas as dependências são thread-safe
// O estado é no banco de dados, não na memória da instância
}
}
Quando nova instância é iniciada, ela pode processar requisições sem aquecimento prévio.
31.16.2 Circuit Breaker e Timeout
Cada integração externa tem circuit breaker e timeout:
@Configuration
public class ResilienceConfig {
@Bean
public Retry seiRetry() {
return Retry.ofDefaults("sei-retry")
.builder()
.maxAttempts(3)
.intervalFunction(IntervalFunction.ofExponentialBackoff(100, 2))
.build();
}
@Bean
public TimeLimiter seiTimeLimiter() {
return TimeLimiter.of("sei-timeout")
.builder()
.timeoutDuration(Duration.ofSeconds(10))
.cancelRunningFuture(true)
.build();
}
}
@Service
public class SeiIntegrationService {
@Retry(name = "sei-retry")
@TimeLimiter(name = "sei-timeout")
@CircuitBreaker(name = "sei-circuit")
public void registrarDocumento(SolicitacaoId solicitacaoId) {
// Falha rápido se SEI!MG está lento
}
}
Se SEI!MG responde lentamente, o timeout encerra a chamada após 10 segundos em vez de travar a requisição indefinidamente.
31.17 Evolução e Versionamento
31.17.1 Versionamento de APIs
APIs são versionadas via header ou URL:
// Via URL
@RestController
@RequestMapping("/api/v1/solicitacoes")
public class SolicitacaoControllerV1 {
@GetMapping("/{id}")
public SolicitacaoDTOV1 obter(@PathVariable String id) { }
}
@RestController
@RequestMapping("/api/v2/solicitacoes")
public class SolicitacaoControllerV2 {
@GetMapping("/{id}")
public SolicitacaoDTOV2 obter(@PathVariable String id) { }
}
// Via header
@RestController
@RequestMapping("/api/solicitacoes")
public class SolicitacaoController {
@GetMapping(value = "/{id}", headers = "Accept-Version=1")
public SolicitacaoDTOV1 obterV1(@PathVariable String id) { }
@GetMapping(value = "/{id}", headers = "Accept-Version=2")
public SolicitacaoDTOV2 obterV2(@PathVariable String id) { }
}
Versão antiga permanece disponível por período de transição. Clientes são notificados com antecedência.
31.17.2 Feature Toggles
Novo recurso é deployado desativado e ativado gradualmente:
@Service
public class SolicitacaoService {
@Autowired
private FeatureToggleService toggleService;
public SolicitacaoDTO criar(CriarSolicitacaoCommand cmd) {
if (toggleService.estaAtivo("solicitacao:assinatura-digital")) {
// Novo fluxo com assinatura digital
return criarComAssinatura(cmd);
} else {
// Fluxo legado
return criarSemAssinatura(cmd);
}
}
}
O administrador ativa feature para 10% dos usuários, depois 50%, depois 100%. Se houver problema, rollback é apenas um click.
31.18 Integração com LLM Gateway e IA
31.18.1 Abstração de Provedores
O LLM Gateway abstrai OpenAI, Anthropic, Google e provedores nacionais:
@Service
public class ClassificacaoAutomaticaService {
private final LlmGatewayClient llmGateway;
public ClassificacaoDTO classificar(String textoSolicitacao) {
LlmRequest request = new LlmRequest()
.setModel("classification-v1")
.setPrompt("Classifique esta solicitação: " + textoSolicitacao)
.setTemperature(0.2); // Determinístico
LlmResponse response = llmGateway.infer(request);
return new ClassificacaoDTO(
response.getChoice("categoria"),
response.getConfidence(),
response.getTokensUsed()
);
}
}
O gateway determina qual provedor usar baseado em: capacidade necessária, custo, latência, residência de dados, conformidade com LGPD.
31.18.2 RAG — Retrieval Augmented Generation
Documentos são indexados em banco vetorial. Quando cidadão faz pergunta, o Copiloto recupera documentos relevantes e passa para LLM:
@Service
public class CopilotoService {
private final VectorStoreClient vectorStore;
private final LlmGatewayClient llm;
public String responder(String pergunta) {
// Recuperar documentos similares via embeddings
List<Documento> documentosRelevantes = vectorStore.buscarPorSemantica(pergunta, topK = 5);
// Montar contexto
String contexto = documentosRelevantes.stream()
.map(Documento::getConteudo)
.collect(joining("\n---\n"));
// Chamar LLM com contexto
LlmRequest request = new LlmRequest()
.setModel("copiloto-v1")
.setSystemPrompt("Você é um assistente do portal de cidadão. Responda com base no contexto fornecido.")
.setUserPrompt("Contexto:\n" + contexto + "\n\nPergunta: " + pergunta);
LlmResponse response = llm.infer(request);
return response.getChosenText();
}
}
RAG evita alucinações e fornece respostas baseadas em informações verificadas da plataforma.
31.19 Trade-offs e Decisões
Arquitetura Hexagonal vs. Arquitetura Limpa: Hexagonal é usada porque alinha bem com DDD e facilita testes. Limpa seria igualmente válida — a escolha foi por convenção da equipe.
Monolito vs. Microsserviços: A plataforma inicia como monolito modular (packages bem delimitados). Quando um módulo atinge limites de escala independente, é extraído como microsserviço — sem big bang refactor.
RestTemplate vs. RestClient vs. Feign: RestClient foi escolhido por sintaxe limpa e não requer interface (Feign). RestTemplate ainda é suportado em código legado.
Spring Data JPA vs. Querydsl vs. jOOQ: JPA é padrão. Querydsl é usado para queries dinâmicas complexas. Quando SQL customizado é necessário, jOOQ é opção.
Single Database vs. Database per Service: Atualmente, banco único com tenant como chave. Conforme serviços evoluem, database per service é roadmap — com saga pattern para transações distribuídas.
31.20 Riscos e Mitigações
| Risco | Consequência | Mitigação |
|---|---|---|
| N+1 queries em listas | Latência exponencial | eager loading configurado; uso de @Query customizado |
| Cache inconsistente | Dados estale mostrados | TTL apropriado; invalidação explícita; eventos de sincronização |
| Retry em cascata | Multiplicação de carga | Circuito aberto após N falhas; timeout firme |
| Memory leak em connections | Exaustão de recursos | Pool de conexões com máximo; monitoramento de leaks |
| Deadlock no banco | Transações travadas | Timeout de transação; ordenação consistente de locks |
| Falha ao publicar evento | Estado inconsistente | Transactional outbox pattern; reconciliação assíncrona |
31.21 Rastreabilidade PRODEMGE
Edital CP 001/2026:
- Item 3.2.2 (Funcionalidades técnicas de backend orientado a serviços)
- Item 3.1.2 (Arquitetura modular e escalável)
Plano de Negócio (Anexo I):
- Seção 3.1.2 (Orientação a serviços)
- Seção 3.1.7 (Segurança e arquitetura)
- Seção 3.3.1 (Escalabilidade)
Funcionalidades (Anexo III):
- Bloco 1 (Integração com 60+ sistemas)
- Bloco 2 (Catálogo de Serviços)
- Bloco 3 (Solicitações)
- Bloco 4 (BPM, Analytics, IA)
- Bloco 6 (Segurança, autorização, auditoria)
Capacidades (Anexo IV):
- Item 3.1.2 (Arquitetura de microsserviços)
- Item 3.1.7 (Separação de responsabilidades)
- Item 3.3.1 (Escalabilidade horizontal)
- Item 3.3.2 (Resiliência e circuit breaker)
- Item 3.3.3 (Observabilidade com OpenTelemetry)
Sustentabilidade (Anexo V):
- Item 2.1 (Manutenibilidade com padrões claros e DDD)
- Item 2.5 (Arquitetura evolutiva com versionamento de APIs)
Esclarecimentos pertinentes:
- Montreal (02/07/2026), Bloco 1: Modularidade incremental confirmada
- Valtech (03/07/2026): Stack Java + Spring Boot, arquitetura orientada a serviços
31.22 Benefícios do Backend em Java e Spring Boot
- Ecosystem maduro: Spring Boot reduz boilerplate e fornece convenções; comunidade vasta
- DDD e Arquitetura Hexagonal: Código desacoplado e testável
- Tratamento de transações: Anotações
@Transactionalsimplificam gerenciamento de ACID - Integração multi-camadas: Cache, mensageria, busca, IA todos integrados
- Observabilidade nativa: Logging estruturado, métricas, tracing automático
- Segurança em profundidade: Validação em múltiplas camadas, proteção contra ataques comuns
- Escalabilidade: Stateless design permite escalar horizontalmente
- Evolução: Versionamento de APIs e feature toggles facilitam deploy seguro
31.23 Decisões Arquiteturais — ADR
| ADR | Tema |
|---|---|
| ADR-001 | Java + Spring Boot como framework backend |
| ADR-032 | Arquitetura Hexagonal com DDD para serviços de domínio |
| ADR-033 | BFF (Backend for Frontend) por canal quando necessário |
| ADR-034 | Repository pattern com Spring Data JPA |
| ADR-035 | Cache distribuído com Redis e namespace por tenant |
| ADR-036 | Mensageria com RabbitMQ, topic exchanges, DLQ |
| ADR-037 | Busca com ElasticSearch indexado por tenant |
| ADR-038 | Circuit Breaker e Retry com Resilience4j |
| ADR-039 | OpenTelemetry para logging, métricas e tracing |
| ADR-040 | Feature toggles para deploy seguro e gradual |
31.24 Próximo Capítulo
O Capítulo 32 — Frontend Web: React detalha a camada de apresentação do Portal, incluindo: componentes React, estado com Redux/Zustand, chamadas de API, autenticação no cliente, acessibilidade, performance e integração com o Painel do Gestor.
Capítulo 30 — Gestão Multi-Tenant
Este capítulo detalha a arquitetura de gestão multi-tenant da Plataforma de Relacionamento Digital com o Cidadão, descrevendo como múltiplos órgãos públicos (tenants) compartilham a mesma infraestrutura e codebase sem co…
Capítulo 32 — Frontend Web: React
Este capítulo detalha a arquitetura e os padrões de implementação do frontend web da Plataforma de Relacionamento Digital com o Cidadão. O frontend web engloba dois contextos distintos: o Portal Web do Cidadão (jornadas…