Documento MestrePRODEMGE
Parte V — Tecnologia
Parte V — TecnologiaCapítulo 31 Revisado

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

RiscoConsequênciaMitigação
N+1 queries em listasLatência exponencialeager loading configurado; uso de @Query customizado
Cache inconsistenteDados estale mostradosTTL apropriado; invalidação explícita; eventos de sincronização
Retry em cascataMultiplicação de cargaCircuito aberto após N falhas; timeout firme
Memory leak em connectionsExaustão de recursosPool de conexões com máximo; monitoramento de leaks
Deadlock no bancoTransações travadasTimeout de transação; ordenação consistente de locks
Falha ao publicar eventoEstado inconsistenteTransactional 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 @Transactional simplificam 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

ADRTema
ADR-001Java + Spring Boot como framework backend
ADR-032Arquitetura Hexagonal com DDD para serviços de domínio
ADR-033BFF (Backend for Frontend) por canal quando necessário
ADR-034Repository pattern com Spring Data JPA
ADR-035Cache distribuído com Redis e namespace por tenant
ADR-036Mensageria com RabbitMQ, topic exchanges, DLQ
ADR-037Busca com ElasticSearch indexado por tenant
ADR-038Circuit Breaker e Retry com Resilience4j
ADR-039OpenTelemetry para logging, métricas e tracing
ADR-040Feature 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.

On this page

31.1 Objetivo do Capítulo31.2 Stack Técnico Confirmado31.2.1 Linguagem e Framework31.2.2 Bibliotecas Principais31.2.3 Versioning e Compatibilidade31.3 Princípios Arquiteturais do Backend31.3.1 Hexagonal (Ports & Adapters)31.3.2 Domain-Driven Design (DDD)31.3.3 Inversão de Controle e Injeção de Dependências31.4 Estrutura de Pacotes e Organização do Código31.4.1 Organização por Domínio (Feature Packages)31.4.2 Configuração por Profiles31.5 Gerenciamento de Identidade e Contexto31.5.1 TenantContext como ThreadLocal31.5.2 Propagação em Chamadas Assíncronas31.6 Camada de Persistência com JPA e Hibernate31.6.1 Entidades JPA e Mapeamento31.6.2 Specifications para Queries Dinâmicas31.6.3 Soft Delete e Data Retention31.7 Gestão de Transações31.7.1 Transacional por Padrão31.7.2 Propagação de Transação e Read-Only31.8 Cache Distribuído com Redis31.8.1 Estratégia de Cache31.8.2 Serialização e Namespace31.8.3 Prevenção de Cache Stampede31.9 Mensageria com RabbitMQ31.9.1 Topologia de Exchanges e Filas31.9.2 Publishers e Subscribers31.9.3 Dead Letter Queue (DLQ) e Retry31.10 Busca Semântica com ElasticSearch31.10.1 Indexação de Documentos31.10.2 Sincronização de Índice31.11 Chamadas a Serviços Externos31.11.1 HTTP Client com RestClient31.11.2 Resiliência com Circuit Breaker31.12 Camada de Aplicação (Use Cases)31.12.1 Commands e Queries31.12.2 Validação em Múltiplas Camadas31.13 Testes e Cobertura31.13.1 Teste Unitário de Domínio31.13.2 Teste de Integração com Testcontainers31.14 Observabilidade Integrada31.14.1 Logging Estruturado31.14.2 Métricas com Micrometer31.14.3 Tracing Distribuído com OpenTelemetry31.15 Segurança na Camada de Aplicação31.15.1 Validação de Entrada31.15.2 Proteção contra CSRF e XSS31.16 Escalabilidade Horizontal31.16.1 Stateless Design31.16.2 Circuit Breaker e Timeout31.17 Evolução e Versionamento31.17.1 Versionamento de APIs31.17.2 Feature Toggles31.18 Integração com LLM Gateway e IA31.18.1 Abstração de Provedores31.18.2 RAG — Retrieval Augmented Generation31.19 Trade-offs e Decisões31.20 Riscos e Mitigações31.21 Rastreabilidade PRODEMGE31.22 Benefícios do Backend em Java e Spring Boot31.23 Decisões Arquiteturais — ADR31.24 Próximo Capítulo