Pular para conteúdo

Lição 1: unidade federativa

Nesta lição, você vai gerar o projeto e implementar, camada por camada, o agregado unidade federativa do subdomínio Geografia. Ao final, o projeto corresponde ao diretório exemplo1 do repositório de D5.

Antes de começar, confira os pré-requisitos do tutorial.

Projeto Maven

Em vez de criar o projeto manualmente, use o gerador, que produz a estrutura de diretórios, os arquivos pom.xml e as classes de infraestrutura que não dependem do domínio, seguindo as convenções. Informe:

  • groupId: com.example;
  • Nome do projeto: exemplo;
  • Descrição: Projeto de exemplo do tutorial de D5;
  • Versão: o valor sugerido, 1.0.0-SNAPSHOT;
  • Subdomínios: informe o primeiro e clique em + para acrescentar os campos do segundo. Cada subdomínio tem nome, tipo e descrição:
    • nome geografia, tipo Subdomínio de suporte e descrição Unidades federativas e municípios do Brasil;
    • nome crm, tipo Subdomínio principal e descrição Relacionamento com os clientes;
  • Usar o framework D5: marcado;
  • Incluir AGENTS.md e skills: desmarcado, porque este tutorial não utiliza agentes de IA;
  • Frontend: Angular.

Extraia o arquivo exemplo.zip obtido. O projeto gerado tem os módulos:

  • exemplo-dominio-compartilhado, do qual os módulos correspondentes a subdomínios dependem e que vai conter os conceitos que não pertencem a nenhum subdomínio específico e são comuns a todos eles, como os papéis de autorização; veja Dependências entre módulos de domínio;
  • exemplo-dominio-geografia, correspondente ao subdomínio Geografia;
  • exemplo-dominio-crm, correspondente ao subdomínio CRM, que permanece vazio até a lição 3, quando o cliente é criado;
  • exemplo-aplicacao, com os serviços de aplicação, projeções e consultas de todos os subdomínios;
  • exemplo-infraestrutura, com as implementações JPA dos repositórios dos agregados de todos os subdomínios;
  • exemplo-ui-backend, com os controladores REST que expõem os agregados de todos os subdomínios e o main da aplicação Spring Boot;
  • exemplo-ui-frontend, com a aplicação Angular que oferece a interface com o usuário. Este tutorial trata apenas do backend: não altere esse módulo.

Os módulos correspondem às camadas da arquitetura limpa: domínio, aplicação, infraestrutura e UI. Diferente do domínio, que é decomposto em um módulo por subdomínio porque cada um tem suas próprias regras de negócio, a aplicação, a infraestrutura e o backend são camadas técnicas transversais que consultam, persistem e expõem os agregados de todos os subdomínios da mesma forma — por isso cada uma é um único módulo.

Diretórios

Cada módulo Java segue a estrutura padrão do Maven, com src/main/java, src/main/resources, src/test/java e src/test/resources. Já o módulo ui-frontend contém a aplicação Angular em src/main/angular:

exemplo
├── pom.xml
├── dominio-compartilhado
├── dominio-geografia
├── dominio-crm
├── aplicacao
├── infraestrutura
├── ui-backend
└── ui-frontend

Arquivos pom.xml

O pom.xml do projeto raiz herda de d5-pai — que define Java 25 e Spring Boot 4.1 e gerencia as versões dos módulos do framework, declarados, por isso, sem <version> — e declara os sete módulos.

O d5-pai é publicado no repositório de snapshots do Maven Central, e não no repositório principal, porque a versão atual, 1.0.0-SNAPSHOT, ainda não é uma versão final. Por isso, o pom.xml do projeto raiz declara esse repositório em <repositories>; os módulos o herdam.

exemplo/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>dev.sauloaraujo</groupId>
        <artifactId>d5-pai</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>exemplo-pai</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <packaging>pom</packaging>

    <repositories>
        <repository>
            <id>central-snapshots</id>
            <url>https://central.sonatype.com/repository/maven-snapshots/</url>
            <releases>
                <enabled>false</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>

    <modules>
        <module>dominio-compartilhado</module>
        <module>dominio-crm</module>
        <module>dominio-geografia</module>
        <module>aplicacao</module>
        <module>infraestrutura</module>
        <module>ui-backend</module>
        <module>ui-frontend</module>
    </modules>
</project>

Cada módulo herda de exemplo-pai. O compartilhado depende de d5-framework-dominio; o de subdomínio depende apenas do compartilhado, através do qual recebe d5-framework-dominio. A aplicação depende de d5-framework-aplicacao e dos módulos de subdomínio cujos serviços de aplicação implementa. A infraestrutura depende de d5-framework-infraestrutura e do módulo de aplicação cujos repositórios de aplicação implementa — os módulos de subdomínio chegam transitivamente por ele. O backend depende de d5-framework-ui-backend, do módulo de aplicação cujos controladores expõem, do domínio compartilhado — cujos papéis de autorização a configuração de segurança usa — e, apenas em tempo de execução, do módulo de infraestrutura — que fornece as implementações concretas dos repositórios — e de d5-framework-springdoc, que gera a documentação OpenAPI da API REST. Essas dependências seguem a regra da dependência da arquitetura limpa: nenhum módulo de domínio ou de aplicação depende da infraestrutura ou da UI. Por fim, o frontend tem o packaging pom e não depende de nenhum outro módulo — o Maven não o compila, e a aplicação Angular é construída com as ferramentas do próprio Angular.

Cada módulo depende também, em escopo de teste, do módulo de teste do framework correspondente à sua camada. Além disso, o backend declara o spring-boot-maven-plugin, que empacota a aplicação em um jar executável e oferece o objetivo spring-boot:run, usado mais adiante, e, como dependência opcional, o Spring Boot DevTools, que reinicia a aplicação automaticamente quando o código é recompilado durante o desenvolvimento.

O diagrama a seguir ilustra essas dependências, tanto entre os módulos do exemplo quanto com os módulos do framework, omitindo o frontend, que não tem nenhuma; uma seta de A para B indica que A depende de B, e uma seta tracejada indica uma dependência restrita ao tempo de execução. Por enquanto, exemplo-dominio-crm depende apenas do domínio compartilhado; a dependência dele em exemplo-dominio-geografia só é acrescentada na lição 3:

graph TD
    subgraph Framework
        FD[d5-framework-dominio]
        FA[d5-framework-aplicacao]
        FI[d5-framework-infraestrutura]
        FU[d5-framework-ui-backend]
        FS[d5-framework-springdoc]
    end

    subgraph exemplo
        DC[exemplo-dominio-compartilhado]
        DG[exemplo-dominio-geografia]
        DR[exemplo-dominio-crm]
        AP[exemplo-aplicacao]
        IN[exemplo-infraestrutura]
        UI[exemplo-ui-backend]
    end

    DC --> FD
    DG --> DC
    DR --> DC
    AP --> FA
    AP --> DG
    AP --> DR
    IN --> FI
    IN --> AP
    UI --> FU
    UI --> AP
    UI --> DC
    UI -.-> FS
    UI -.-> IN
exemplo/dominio-compartilhado/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.example</groupId>
        <artifactId>exemplo-pai</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>exemplo-dominio-compartilhado</artifactId>

    <dependencies>
        <dependency>
            <groupId>dev.sauloaraujo</groupId>
            <artifactId>d5-framework-dominio</artifactId>
        </dependency>

        <dependency>
            <groupId>dev.sauloaraujo</groupId>
            <artifactId>d5-framework-dominio-teste</artifactId>
            <type>pom</type>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
exemplo/dominio-geografia/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.example</groupId>
        <artifactId>exemplo-pai</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>exemplo-dominio-geografia</artifactId>

    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>exemplo-dominio-compartilhado</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>dev.sauloaraujo</groupId>
            <artifactId>d5-framework-dominio-teste</artifactId>
            <type>pom</type>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
exemplo/dominio-crm/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.example</groupId>
        <artifactId>exemplo-pai</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>exemplo-dominio-crm</artifactId>

    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>exemplo-dominio-compartilhado</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>dev.sauloaraujo</groupId>
            <artifactId>d5-framework-dominio-teste</artifactId>
            <type>pom</type>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
exemplo/aplicacao/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.example</groupId>
        <artifactId>exemplo-pai</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>exemplo-aplicacao</artifactId>

    <dependencies>
        <dependency>
            <groupId>dev.sauloaraujo</groupId>
            <artifactId>d5-framework-aplicacao</artifactId>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>exemplo-dominio-crm</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>exemplo-dominio-geografia</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>dev.sauloaraujo</groupId>
            <artifactId>d5-framework-aplicacao-teste</artifactId>
            <type>pom</type>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
exemplo/infraestrutura/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.example</groupId>
        <artifactId>exemplo-pai</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>exemplo-infraestrutura</artifactId>

    <dependencies>
        <dependency>
            <groupId>dev.sauloaraujo</groupId>
            <artifactId>d5-framework-infraestrutura</artifactId>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>exemplo-aplicacao</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>dev.sauloaraujo</groupId>
            <artifactId>d5-framework-infraestrutura-teste</artifactId>
            <type>pom</type>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
exemplo/ui-backend/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.example</groupId>
        <artifactId>exemplo-pai</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>exemplo-ui-backend</artifactId>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>dev.sauloaraujo</groupId>
            <artifactId>d5-framework-ui-backend</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>exemplo-aplicacao</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>exemplo-dominio-compartilhado</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>dev.sauloaraujo</groupId>
            <artifactId>d5-framework-ui-backend-teste</artifactId>
            <type>pom</type>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>dev.sauloaraujo</groupId>
            <artifactId>d5-framework-springdoc</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>exemplo-infraestrutura</artifactId>
            <version>${project.version}</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
</project>
exemplo/ui-frontend/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.example</groupId>
        <artifactId>exemplo-pai</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>exemplo-ui-frontend</artifactId>

    <packaging>pom</packaging>
</project>

Unidade federativa

Uma unidade federativa é identificada por um código IBGE. Além disso, possui sigla e nome únicos. A sigla e o código IBGE são objetos de valor associados ao agregado unidade federativa. Seguindo as convenções, o pacote dos três é com.example.exemplo.dominio.geografia.unidadefederativa. Crie o package-info.java desse pacote, anotado com @NullMarked:

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/unidadefederativa/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.dominio.geografia.unidadefederativa;

Domínio

Sigla

A sigla é formada por duas letras maiúsculas. Portanto, seu formato pode ser definido através de uma expressão regular. Objetos de valor desse tipo herdam de ObjetoValorRegexAbstrato.

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/unidadefederativa/UnidadeFederativaSigla.java
package com.example.exemplo.dominio.geografia.unidadefederativa;

import dev.sauloaraujo.d5.framework.dominio.impl.ObjetoValorRegexAbstrato;

public final class UnidadeFederativaSigla extends ObjetoValorRegexAbstrato {
    public static final int TAMANHO = 2;

    private static final String FORMATO = "[A-Z]{" + TAMANHO + "}";
    private static final String MENSAGEM = "A sigla da unidade federativa deve conter " + TAMANHO
            + " letras maiúsculas";

    public UnidadeFederativaSigla(String valor) {
        super(valor, FORMATO, MENSAGEM);
    }
}

Código IBGE

O código IBGE é formado por dois dígitos e, portanto, também herda de ObjetoValorRegexAbstrato. Adicionalmente, como o código IBGE identifica uma unidade federativa, sua classe implementa IIdentificador.

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/unidadefederativa/UnidadeFederativaCodigoIbge.java
package com.example.exemplo.dominio.geografia.unidadefederativa;

import dev.sauloaraujo.d5.framework.dominio.IIdentificador;
import dev.sauloaraujo.d5.framework.dominio.impl.ObjetoValorRegexAbstrato;

public final class UnidadeFederativaCodigoIbge extends ObjetoValorRegexAbstrato implements IIdentificador {
    public static final int TAMANHO = 2;

    private static final String FORMATO = "\\d{" + TAMANHO + "}";
    private static final String MENSAGEM = "O código IBGE da unidade federativa deve conter " + TAMANHO + " dígitos";

    public UnidadeFederativaCodigoIbge(String valor) {
        super(valor, FORMATO, MENSAGEM);
    }
}

Agregado

Unidade federativa, código IBGE e sigla formam um agregado, com UnidadeFederativa como raiz — por isso ela implementa IAgregado, parametrizada pelo tipo do identificador. Seu construtor valida os argumentos, rejeitando um nome em branco. Como as demais classes do domínio, ela é final.

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/unidadefederativa/UnidadeFederativa.java
package com.example.exemplo.dominio.geografia.unidadefederativa;

import java.util.Objects;
import java.util.Optional;

import org.apache.commons.lang3.Validate;

import dev.sauloaraujo.d5.framework.dominio.IAgregado;

public final class UnidadeFederativa implements IAgregado<UnidadeFederativaCodigoIbge> {
    private final UnidadeFederativaCodigoIbge identificador;
    private final UnidadeFederativaSigla sigla;
    private final String nome;

    public UnidadeFederativa(UnidadeFederativaCodigoIbge identificador, UnidadeFederativaSigla sigla, String nome) {
        this.identificador = Objects.requireNonNull(identificador);
        this.sigla = Objects.requireNonNull(sigla);
        this.nome = Validate.notBlank(nome, "Nome da unidade federativa em branco");
    }

    @Override
    public Optional<UnidadeFederativaCodigoIbge> getIdentificador() {
        return Optional.of(identificador);
    }

    public UnidadeFederativaSigla getSigla() {
        return sigla;
    }

    public String getNome() {
        return nome;
    }
}

Repositório

Serviços acessam unidades federativas persistidas através de um repositório. Seguindo as convenções, a interface do repositório fica no pacote com.example.exemplo.dominio.geografia.unidadefederativa.spi — o domínio declara a interface, e a infraestrutura a implementa; veja Inversão de dependência e pacotes spi — e estende IAgregadoRepositorio. Adicionalmente, expõe existePorSigla e existePorNome, que o serviço usa para garantir que a sigla e o nome de uma unidade federativa sejam únicos. Ambos recebem o identificador da própria unidade federativa a ignorar na verificação, para que alterá-la não a faça conflitar consigo mesma. Crie o package-info.java desse pacote, anotado com @NullMarked:

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/unidadefederativa/spi/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.dominio.geografia.unidadefederativa.spi;
exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/unidadefederativa/spi/IUnidadeFederativaRepositorio.java
package com.example.exemplo.dominio.geografia.unidadefederativa.spi;

import java.util.Optional;

import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;

import dev.sauloaraujo.d5.framework.dominio.spi.IAgregadoRepositorio;

public interface IUnidadeFederativaRepositorio
        extends IAgregadoRepositorio<UnidadeFederativaCodigoIbge, UnidadeFederativa> {
    boolean existePorSigla(UnidadeFederativaSigla sigla, Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado);

    boolean existePorNome(String nome, Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado);
}

Autorização

As regras de autorização sobre um agregado usam papéis compartilhados por todos os subdomínios. Seguindo as convenções, o gerador já criou, no módulo exemplo-dominio-compartilhado, pacote com.example.exemplo.dominio, um enum que os declara, e o package-info.java desse pacote, anotado com @NullMarked:

exemplo/dominio-compartilhado/src/main/java/com/example/exemplo/dominio/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.dominio;
exemplo/dominio-compartilhado/src/main/java/com/example/exemplo/dominio/Autorizacao.java
package com.example.exemplo.dominio;

public enum Autorizacao {
    ADMINISTRADOR, USUARIO
}

Serviço

Operações sobre unidades federativas são encapsuladas em um serviço que estende AgregadoServicoAbstrato. Ele implementa criarAutorizacoes consultando o ISegurancaServico recebido no construtor para verificar se o usuário possui a autorização ADMINISTRADOR, devolvendo um AgregadoAutorizacoes que libera a busca a qualquer usuário e as demais operações somente a administradores. Também sobrescreve validarInclusaoAlteracao para usar existePorSigla e existePorNome, ignorando o identificador da própria unidade federativa, e impedir que uma inclusão ou alteração deixe duas unidades federativas com a mesma sigla ou o mesmo nome.

Note que o serviço não leva nenhuma anotação do Spring: ele, os repositórios e o serviço de aplicação são registrados como beans automaticamente.

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/unidadefederativa/UnidadeFederativaServico.java
package com.example.exemplo.dominio.geografia.unidadefederativa;

import java.util.Objects;
import java.util.Optional;

import com.example.exemplo.dominio.Autorizacao;
import com.example.exemplo.dominio.geografia.unidadefederativa.spi.IUnidadeFederativaRepositorio;

import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoAutorizacoes;
import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoServicoAbstrato;
import dev.sauloaraujo.d5.framework.dominio.spi.ISegurancaServico;

public final class UnidadeFederativaServico extends
        AgregadoServicoAbstrato<UnidadeFederativaCodigoIbge, UnidadeFederativa, AgregadoAutorizacoes<UnidadeFederativa>> {
    public static final String IDENTIFICADOR_INEXISTENTE_MODELO = "Não existe unidade federativa com o identificador %s";
    public static final String IDENTIFICADOR_EXISTENTE_MODELO = "Já existe uma unidade federativa com o identificador %s";
    public static final String SIGLA_EXISTENTE_MODELO = "Já existe uma unidade federativa com a sigla %s";
    public static final String NOME_EXISTENTE_MODELO = "Já existe uma unidade federativa com o nome %s";

    private final IUnidadeFederativaRepositorio repositorio;

    public UnidadeFederativaServico(ISegurancaServico segurancaServico, IUnidadeFederativaRepositorio repositorio) {
        super(segurancaServico, repositorio, IDENTIFICADOR_EXISTENTE_MODELO, IDENTIFICADOR_INEXISTENTE_MODELO);

        this.repositorio = Objects.requireNonNull(repositorio);
    }

    @Override
    public AgregadoAutorizacoes<UnidadeFederativa> criarAutorizacoes(
            Optional<UnidadeFederativaCodigoIbge> identificador) {
        Objects.requireNonNull(identificador);

        var administrador = getSegurancaServico().possuiAutorizacao(Autorizacao.ADMINISTRADOR.name());
        return new AgregadoAutorizacoes<>(administrador, true, administrador, administrador);
    }

    @Override
    protected void validarInclusaoAlteracao(UnidadeFederativa agregado) {
        Objects.requireNonNull(agregado);

        var identificador = agregado.getIdentificador();

        validarInexistencia(sigla -> repositorio.existePorSigla(sigla, identificador), agregado.getSigla(),
                SIGLA_EXISTENTE_MODELO);

        validarInexistencia(nome -> repositorio.existePorNome(nome, identificador), agregado.getNome(),
                NOME_EXISTENTE_MODELO);
    }
}

Testes

O comportamento do serviço é especificado com cenários em Gherkin e automatizado com Cucumber. O gerador já criou a suíte que conecta o Cucumber ao JUnit Platform e seleciona os cenários em src/test/resources — todo módulo de subdomínio tem a sua:

exemplo/dominio-geografia/src/test/java/com/example/exemplo/dominio/geografia/CucumberTests.java
package com.example.exemplo.dominio.geografia;

import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectDirectories;
import org.junit.platform.suite.api.Suite;

import io.cucumber.junit.platform.engine.Constants;

@Suite(failIfNoTests = false)
@IncludeEngines("cucumber")
@ConfigurationParameter(key = Constants.GLUE_PROPERTY_NAME, value = "com.example.exemplo.dominio.geografia")
@SelectDirectories("src/test/resources")
public class CucumberTests {
}

Escreva os cenários cobrindo inclusão, busca, alteração e exclusão de uma unidade federativa, incluindo as regras de autorização e de unicidade de sigla e nome. O comentário # language: pt, na primeira linha, faz o Cucumber reconhecer as palavras-chave do Gherkin em português — Funcionalidade, Contexto, Cenário, Dado, Quando, Então e E:

exemplo/dominio-geografia/src/test/resources/features/unidade-federativa.feature
# language: pt
Funcionalidade: Gerenciar unidades federativas
  Para manter o cadastro de unidades federativas da aplicação
  Como usuário do sistema
  Quero incluir, buscar, alterar e excluir unidades federativas,
  respeitando as regras de unicidade de sigla e nome e de autorização

  Contexto:
    Dado que não existe nenhuma unidade federativa cadastrada

  Cenário: Incluir uma unidade federativa
    Dado que sou um usuário administrador
    Quando eu incluo a unidade federativa de código IBGE "35", sigla "SP" e nome "São Paulo"
    Então a unidade federativa é incluída com sucesso
    E a unidade federativa de código IBGE "35" passa a existir

  Cenário: Não é possível incluir uma unidade federativa sem autorização de administrador
    Dado que sou um usuário sem autorização de administrador
    Quando eu tento incluir a unidade federativa de código IBGE "35", sigla "SP" e nome "São Paulo"
    Então a inclusão é rejeitada por falta de autorização

  Cenário: Não é possível incluir uma unidade federativa com código IBGE já existente
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu tento incluir a unidade federativa de código IBGE "35", sigla "RJ" e nome "Rio de Janeiro"
    Então a inclusão é rejeitada porque já existe uma unidade federativa com o identificador "35"

  Cenário: Não é possível incluir uma unidade federativa com sigla já existente
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu tento incluir a unidade federativa de código IBGE "33", sigla "SP" e nome "Rio de Janeiro"
    Então a inclusão é rejeitada porque já existe uma unidade federativa com a sigla "SP"

  Cenário: Não é possível incluir uma unidade federativa com nome já existente
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu tento incluir a unidade federativa de código IBGE "33", sigla "RJ" e nome "São Paulo"
    Então a inclusão é rejeitada porque já existe uma unidade federativa com o nome "São Paulo"

  Cenário: Não é possível incluir uma unidade federativa com nome já existente, ainda que com outras maiúsculas e minúsculas
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu tento incluir a unidade federativa de código IBGE "33", sigla "RJ" e nome "SÃO PAULO"
    Então a inclusão é rejeitada porque já existe uma unidade federativa com o nome "SÃO PAULO"

  Cenário: Buscar uma unidade federativa existente
    Dado que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    E que sou um usuário sem autorização de administrador
    Quando eu busco a unidade federativa de código IBGE "35"
    Então a unidade federativa encontrada tem sigla "SP" e nome "São Paulo"

  Cenário: Não é possível buscar uma unidade federativa inexistente
    Dado que sou um usuário sem autorização de administrador
    Quando eu tento buscar a unidade federativa de código IBGE "35"
    Então a busca é rejeitada porque não existe unidade federativa com o identificador "35"

  Cenário: Alterar uma unidade federativa
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu altero a unidade federativa de código IBGE "35" para sigla "SP" e nome "Estado de São Paulo"
    Então a unidade federativa é alterada com sucesso
    E a unidade federativa de código IBGE "35" passa a ter sigla "SP" e nome "Estado de São Paulo"

  Cenário: Alterar uma unidade federativa mantendo sua própria sigla e nome
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu altero a unidade federativa de código IBGE "35" para sigla "SP" e nome "São Paulo"
    Então a unidade federativa é alterada com sucesso

  Cenário: Não é possível alterar uma unidade federativa sem autorização de administrador
    Dado que sou um usuário sem autorização de administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu tento alterar a unidade federativa de código IBGE "35" para sigla "SP" e nome "Estado de São Paulo"
    Então a alteração é rejeitada por falta de autorização

  Cenário: Não é possível alterar uma unidade federativa inexistente
    Dado que sou um usuário administrador
    Quando eu tento alterar a unidade federativa de código IBGE "35" para sigla "SP" e nome "São Paulo"
    Então a alteração é rejeitada porque não existe unidade federativa com o identificador "35"

  Cenário: Não é possível alterar uma unidade federativa para a sigla de outra
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    E que existe uma unidade federativa com código IBGE "33", sigla "RJ" e nome "Rio de Janeiro"
    Quando eu tento alterar a unidade federativa de código IBGE "33" para sigla "SP" e nome "Rio de Janeiro"
    Então a alteração é rejeitada porque já existe uma unidade federativa com a sigla "SP"

  Cenário: Excluir uma unidade federativa
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu excluo a unidade federativa de código IBGE "35"
    Então a unidade federativa é excluída com sucesso
    E a unidade federativa de código IBGE "35" deixa de existir

  Cenário: Não é possível excluir uma unidade federativa sem autorização de administrador
    Dado que sou um usuário sem autorização de administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu tento excluir a unidade federativa de código IBGE "35"
    Então a exclusão é rejeitada por falta de autorização

  Cenário: Não é possível excluir uma unidade federativa inexistente
    Dado que sou um usuário administrador
    Quando eu tento excluir a unidade federativa de código IBGE "35"
    Então a exclusão é rejeitada porque não existe unidade federativa com o identificador "35"

Implemente as step definitions que ligam os cenários ao serviço. Como UnidadeFederativaServico depende das abstrações ISegurancaServico e IUnidadeFederativaRepositorio, use o Mockito para dublá-las: a segurança simula a autorização do usuário, e o repositório simula a persistência através de um mapa em memória, preservando as mesmas regras de unicidade que uma implementação real deve respeitar — inclusive a comparação de nomes que ignora maiúsculas e minúsculas. Cada método anotado com @Dado, @Quando ou @Entao implementa os passos cujo texto corresponde à sua expressão, na qual {string} captura um texto entre aspas e o passa como argumento. O comportamento de cada método dos dublês é definido com when(...).thenAnswer(...) ou, para os métodos void, como alterar e excluir, que não podem ser passados a when, com doAnswer(...).when(...).

exemplo/dominio-geografia/src/test/java/com/example/exemplo/dominio/geografia/unidadefederativa/UnidadeFederativaStepDefinitions.java
package com.example.exemplo.dominio.geografia.unidadefederativa;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import com.example.exemplo.dominio.Autorizacao;
import com.example.exemplo.dominio.geografia.unidadefederativa.spi.IUnidadeFederativaRepositorio;

import dev.sauloaraujo.d5.framework.dominio.AgregadoExistenteExcecao;
import dev.sauloaraujo.d5.framework.dominio.AgregadoInexistenteExcecao;
import dev.sauloaraujo.d5.framework.dominio.spi.ISegurancaServico;
import dev.sauloaraujo.d5.framework.dominio.spi.NaoAutorizadoExcecao;
import io.cucumber.java.pt.Dado;
import io.cucumber.java.pt.Entao;
import io.cucumber.java.pt.Quando;

public final class UnidadeFederativaStepDefinitions {
    private final Map<String, UnidadeFederativa> unidadesFederativas = new HashMap<>();
    private final ISegurancaServico segurancaServico = mock(ISegurancaServico.class);
    private final IUnidadeFederativaRepositorio repositorio = mock(IUnidadeFederativaRepositorio.class);
    private final UnidadeFederativaServico servico = new UnidadeFederativaServico(segurancaServico, repositorio);

    private UnidadeFederativa resultadoInclusao;
    private UnidadeFederativa resultadoBusca;
    private RuntimeException excecaoCapturada;

    public UnidadeFederativaStepDefinitions() {
        when(repositorio.existePorIdentificador(any()))
                .thenAnswer(chamada -> unidadesFederativas.containsKey(chave(chamada.getArgument(0))));

        when(repositorio.buscarPorIdentificador(any())).thenAnswer(chamada -> {
            UnidadeFederativaCodigoIbge codigoIbge = chamada.getArgument(0);
            var unidadeFederativa = unidadesFederativas.get(chave(codigoIbge));
            if (unidadeFederativa == null) {
                throw new AgregadoInexistenteExcecao(
                        String.format(UnidadeFederativaServico.IDENTIFICADOR_INEXISTENTE_MODELO, codigoIbge));
            }
            return unidadeFederativa;
        });

        when(repositorio.existePorSigla(any(), any())).thenAnswer(chamada -> {
            UnidadeFederativaSigla sigla = chamada.getArgument(0);
            Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado = chamada.getArgument(1);
            return unidadesFederativas.values().stream().anyMatch(
                    candidata -> candidata.getSigla().equals(sigla) && !foiIgnorada(candidata, identificadorIgnorado));
        });

        when(repositorio.existePorNome(any(), any())).thenAnswer(chamada -> {
            String nome = chamada.getArgument(0);
            Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado = chamada.getArgument(1);
            return unidadesFederativas.values().stream()
                    .anyMatch(candidata -> candidata.getNome().equalsIgnoreCase(nome)
                            && !foiIgnorada(candidata, identificadorIgnorado));
        });

        when(repositorio.incluir(any())).thenAnswer(chamada -> {
            UnidadeFederativa unidadeFederativa = chamada.getArgument(0);
            unidadesFederativas.put(chave(unidadeFederativa.getIdentificador().orElseThrow()), unidadeFederativa);
            return unidadeFederativa;
        });

        doAnswer(chamada -> {
            UnidadeFederativa unidadeFederativa = chamada.getArgument(0);
            unidadesFederativas.put(chave(unidadeFederativa.getIdentificador().orElseThrow()), unidadeFederativa);
            return null;
        }).when(repositorio).alterar(any());

        doAnswer(chamada -> {
            UnidadeFederativaCodigoIbge codigoIbge = chamada.getArgument(0);
            unidadesFederativas.remove(chave(codigoIbge));
            return null;
        }).when(repositorio).excluir(any());
    }

    private static String chave(UnidadeFederativaCodigoIbge codigoIbge) {
        return codigoIbge.toString();
    }

    private static boolean foiIgnorada(UnidadeFederativa candidata,
            Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado) {
        return identificadorIgnorado.isPresent() && candidata.getIdentificador().equals(identificadorIgnorado);
    }

    private static UnidadeFederativa criar(String codigoIbge, String sigla, String nome) {
        return new UnidadeFederativa(new UnidadeFederativaCodigoIbge(codigoIbge), new UnidadeFederativaSigla(sigla),
                nome);
    }

    @Dado("que não existe nenhuma unidade federativa cadastrada")
    public void que_nao_existe_nenhuma_unidade_federativa_cadastrada() {
        unidadesFederativas.clear();
    }

    @Dado("que sou um usuário administrador")
    public void que_sou_um_usuario_administrador() {
        when(segurancaServico.possuiAutorizacao(Autorizacao.ADMINISTRADOR.name())).thenReturn(true);
    }

    @Dado("que sou um usuário sem autorização de administrador")
    public void que_sou_um_usuario_sem_autorizacao_de_administrador() {
        when(segurancaServico.possuiAutorizacao(Autorizacao.ADMINISTRADOR.name())).thenReturn(false);
    }

    @Dado("que existe uma unidade federativa com código IBGE {string}, sigla {string} e nome {string}")
    public void que_existe_uma_unidade_federativa(String codigoIbge, String sigla, String nome) {
        var unidadeFederativa = criar(codigoIbge, sigla, nome);
        unidadesFederativas.put(chave(unidadeFederativa.getIdentificador().orElseThrow()), unidadeFederativa);
    }

    @Quando("eu incluo a unidade federativa de código IBGE {string}, sigla {string} e nome {string}")
    public void eu_incluo_a_unidade_federativa(String codigoIbge, String sigla, String nome) {
        incluir(codigoIbge, sigla, nome);
    }

    @Quando("eu tento incluir a unidade federativa de código IBGE {string}, sigla {string} e nome {string}")
    public void eu_tento_incluir_a_unidade_federativa(String codigoIbge, String sigla, String nome) {
        incluir(codigoIbge, sigla, nome);
    }

    private void incluir(String codigoIbge, String sigla, String nome) {
        try {
            resultadoInclusao = servico.incluir(criar(codigoIbge, sigla, nome));
        } catch (RuntimeException excecao) {
            excecaoCapturada = excecao;
        }
    }

    @Quando("eu busco a unidade federativa de código IBGE {string}")
    public void eu_busco_a_unidade_federativa(String codigoIbge) {
        buscar(codigoIbge);
    }

    @Quando("eu tento buscar a unidade federativa de código IBGE {string}")
    public void eu_tento_buscar_a_unidade_federativa(String codigoIbge) {
        buscar(codigoIbge);
    }

    private void buscar(String codigoIbge) {
        try {
            resultadoBusca = servico.buscar(new UnidadeFederativaCodigoIbge(codigoIbge));
        } catch (RuntimeException excecao) {
            excecaoCapturada = excecao;
        }
    }

    @Quando("eu altero a unidade federativa de código IBGE {string} para sigla {string} e nome {string}")
    public void eu_altero_a_unidade_federativa(String codigoIbge, String sigla, String nome) {
        alterar(codigoIbge, sigla, nome);
    }

    @Quando("eu tento alterar a unidade federativa de código IBGE {string} para sigla {string} e nome {string}")
    public void eu_tento_alterar_a_unidade_federativa(String codigoIbge, String sigla, String nome) {
        alterar(codigoIbge, sigla, nome);
    }

    private void alterar(String codigoIbge, String sigla, String nome) {
        try {
            servico.alterar(criar(codigoIbge, sigla, nome));
        } catch (RuntimeException excecao) {
            excecaoCapturada = excecao;
        }
    }

    @Quando("eu excluo a unidade federativa de código IBGE {string}")
    public void eu_excluo_a_unidade_federativa(String codigoIbge) {
        excluir(codigoIbge);
    }

    @Quando("eu tento excluir a unidade federativa de código IBGE {string}")
    public void eu_tento_excluir_a_unidade_federativa(String codigoIbge) {
        excluir(codigoIbge);
    }

    private void excluir(String codigoIbge) {
        try {
            servico.excluir(new UnidadeFederativaCodigoIbge(codigoIbge));
        } catch (RuntimeException excecao) {
            excecaoCapturada = excecao;
        }
    }

    @Entao("a unidade federativa é incluída com sucesso")
    public void a_unidade_federativa_e_incluida_com_sucesso() {
        assertNull(excecaoCapturada);
        assertNotNull(resultadoInclusao);
    }

    @Entao("a unidade federativa de código IBGE {string} passa a existir")
    public void a_unidade_federativa_passa_a_existir(String codigoIbge) {
        assertTrue(unidadesFederativas.containsKey(codigoIbge));
    }

    @Entao("a inclusão é rejeitada por falta de autorização")
    public void a_inclusao_e_rejeitada_por_falta_de_autorizacao() {
        assertInstanceOf(NaoAutorizadoExcecao.class, excecaoCapturada);
    }

    @Entao("a inclusão é rejeitada porque já existe uma unidade federativa com o identificador {string}")
    public void a_inclusao_e_rejeitada_pelo_identificador(String codigoIbge) {
        assertMensagemDeExistencia(UnidadeFederativaServico.IDENTIFICADOR_EXISTENTE_MODELO, codigoIbge);
    }

    @Entao("a inclusão é rejeitada porque já existe uma unidade federativa com a sigla {string}")
    public void a_inclusao_e_rejeitada_pela_sigla(String sigla) {
        assertMensagemDeExistencia(UnidadeFederativaServico.SIGLA_EXISTENTE_MODELO, sigla);
    }

    @Entao("a inclusão é rejeitada porque já existe uma unidade federativa com o nome {string}")
    public void a_inclusao_e_rejeitada_pelo_nome(String nome) {
        assertMensagemDeExistencia(UnidadeFederativaServico.NOME_EXISTENTE_MODELO, nome);
    }

    @Entao("a unidade federativa encontrada tem sigla {string} e nome {string}")
    public void a_unidade_federativa_encontrada_tem(String sigla, String nome) {
        assertEquals(new UnidadeFederativaSigla(sigla), resultadoBusca.getSigla());
        assertEquals(nome, resultadoBusca.getNome());
    }

    @Entao("a busca é rejeitada porque não existe unidade federativa com o identificador {string}")
    public void a_busca_e_rejeitada_por_inexistencia(String codigoIbge) {
        assertMensagemDeInexistencia(codigoIbge);
    }

    @Entao("a unidade federativa é alterada com sucesso")
    public void a_unidade_federativa_e_alterada_com_sucesso() {
        assertNull(excecaoCapturada);
    }

    @Entao("a unidade federativa de código IBGE {string} passa a ter sigla {string} e nome {string}")
    public void a_unidade_federativa_passa_a_ter(String codigoIbge, String sigla, String nome) {
        var unidadeFederativa = unidadesFederativas.get(codigoIbge);
        assertNotNull(unidadeFederativa);
        assertEquals(new UnidadeFederativaSigla(sigla), unidadeFederativa.getSigla());
        assertEquals(nome, unidadeFederativa.getNome());
    }

    @Entao("a alteração é rejeitada por falta de autorização")
    public void a_alteracao_e_rejeitada_por_falta_de_autorizacao() {
        assertInstanceOf(NaoAutorizadoExcecao.class, excecaoCapturada);
    }

    @Entao("a alteração é rejeitada porque não existe unidade federativa com o identificador {string}")
    public void a_alteracao_e_rejeitada_por_inexistencia(String codigoIbge) {
        assertMensagemDeInexistencia(codigoIbge);
    }

    @Entao("a alteração é rejeitada porque já existe uma unidade federativa com a sigla {string}")
    public void a_alteracao_e_rejeitada_pela_sigla(String sigla) {
        assertMensagemDeExistencia(UnidadeFederativaServico.SIGLA_EXISTENTE_MODELO, sigla);
    }

    @Entao("a unidade federativa é excluída com sucesso")
    public void a_unidade_federativa_e_excluida_com_sucesso() {
        assertNull(excecaoCapturada);
    }

    @Entao("a unidade federativa de código IBGE {string} deixa de existir")
    public void a_unidade_federativa_deixa_de_existir(String codigoIbge) {
        assertFalse(unidadesFederativas.containsKey(codigoIbge));
    }

    @Entao("a exclusão é rejeitada por falta de autorização")
    public void a_exclusao_e_rejeitada_por_falta_de_autorizacao() {
        assertInstanceOf(NaoAutorizadoExcecao.class, excecaoCapturada);
    }

    @Entao("a exclusão é rejeitada porque não existe unidade federativa com o identificador {string}")
    public void a_exclusao_e_rejeitada_por_inexistencia(String codigoIbge) {
        assertMensagemDeInexistencia(codigoIbge);
    }

    private void assertMensagemDeExistencia(String modelo, Object argumento) {
        assertInstanceOf(AgregadoExistenteExcecao.class, excecaoCapturada);
        assertEquals(String.format(modelo, argumento), excecaoCapturada.getMessage());
    }

    private void assertMensagemDeInexistencia(Object argumento) {
        assertInstanceOf(AgregadoInexistenteExcecao.class, excecaoCapturada);
        assertEquals(String.format(UnidadeFederativaServico.IDENTIFICADOR_INEXISTENTE_MODELO, argumento),
                excecaoCapturada.getMessage());
    }
}

Execute os cenários no diretório exemplo:

mvn -pl dominio-geografia -am test

-pl restringe o build ao módulo dominio-geografia, e -am inclui nele os módulos dos quais esse módulo depende — aqui, dominio-compartilhado —, que, sem essa opção, o Maven procuraria no repositório local. O resultado deve informar 16 testes, um por cenário, sem falhas.

Aplicação

A pesquisa de unidades federativas, ao contrário das demais operações, não passa pelas regras do agregado: ela pertence ao lado de consulta e consulta diretamente uma projeção somente leitura, com paginação, ordenação e filtro. Seguindo as convenções, as classes ficam no módulo exemplo-aplicacao, pacote com.example.exemplo.aplicacao.geografia.unidadefederativa. Crie o package-info.java desse pacote, anotado com @NullMarked:

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/geografia/unidadefederativa/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.aplicacao.geografia.unidadefederativa;

Projeção

A projeção de unidade federativa contém seu identificador, sua sigla e seu nome — o suficiente para, por exemplo, popular uma lista de seleção. Assim como no agregado, o identificador e a sigla são representados por seus objetos de valor; o construtor que recebe Strings existe para que a consulta JPQL possa instanciar a projeção diretamente. Ela implementa IProjecao, parametrizada pelo agregado que representa.

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/geografia/unidadefederativa/UnidadeFederativaProjecao.java
package com.example.exemplo.aplicacao.geografia.unidadefederativa;

import java.util.Objects;

import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;

import dev.sauloaraujo.d5.framework.aplicacao.IProjecao;

public final class UnidadeFederativaProjecao implements IProjecao<UnidadeFederativa> {
    private final UnidadeFederativaCodigoIbge identificador;
    private final UnidadeFederativaSigla sigla;
    private final String nome;

    public UnidadeFederativaProjecao(UnidadeFederativaCodigoIbge identificador, UnidadeFederativaSigla sigla,
            String nome) {
        this.identificador = Objects.requireNonNull(identificador);
        this.sigla = Objects.requireNonNull(sigla);
        this.nome = Objects.requireNonNull(nome);
    }

    public UnidadeFederativaProjecao(String identificador, String sigla, String nome) {
        this(new UnidadeFederativaCodigoIbge(identificador), new UnidadeFederativaSigla(sigla), nome);
    }

    public UnidadeFederativaCodigoIbge getIdentificador() {
        return identificador;
    }

    public UnidadeFederativaSigla getSigla() {
        return sigla;
    }

    public String getNome() {
        return nome;
    }
}

Ordenação

As propriedades pelas quais a projeção pode ser ordenada são declaradas em um enum que implementa IOrdenacao, parametrizado pela projeção.

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/geografia/unidadefederativa/UnidadeFederativaOrdenacao.java
package com.example.exemplo.aplicacao.geografia.unidadefederativa;

import dev.sauloaraujo.d5.framework.aplicacao.IOrdenacao;

public enum UnidadeFederativaOrdenacao implements IOrdenacao<UnidadeFederativaProjecao> {
    IDENTIFICADOR, SIGLA, NOME
}

Consulta

Os parâmetros da pesquisa são representados por uma classe que estende Consulta, acrescentando os atributos sigla e nome, usados para filtrar as unidades federativas por um trecho da sigla e do nome.

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/geografia/unidadefederativa/UnidadeFederativaConsulta.java
package com.example.exemplo.aplicacao.geografia.unidadefederativa;

import java.util.Objects;
import java.util.Optional;

import dev.sauloaraujo.d5.framework.aplicacao.impl.Consulta;

public final class UnidadeFederativaConsulta extends Consulta<UnidadeFederativaOrdenacao> {
    private final Optional<String> sigla;
    private final Optional<String> nome;

    public UnidadeFederativaConsulta(Optional<UnidadeFederativaOrdenacao> ordenacao, Optional<Boolean> descendente,
            Optional<Integer> indicePagina, Optional<Integer> tamanhoPagina, Optional<String> sigla,
            Optional<String> nome) {
        super(ordenacao, descendente, indicePagina, tamanhoPagina);

        this.sigla = Objects.requireNonNull(sigla);
        this.nome = Objects.requireNonNull(nome);
    }

    public Optional<String> getSigla() {
        return sigla;
    }

    public Optional<String> getNome() {
        return nome;
    }
}

Repositório de aplicação

Assim como o repositório do agregado, a pesquisa de projeções é acessada através de um repositório — neste caso, um repositório de aplicação. Sua interface fica no pacote com.example.exemplo.aplicacao.geografia.unidadefederativa.spi e estende IAgregadoAplicacaoRepositorio, parametrizada pela projeção e pela consulta de unidade federativa. Crie o package-info.java desse pacote, anotado com @NullMarked:

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/geografia/unidadefederativa/spi/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.aplicacao.geografia.unidadefederativa.spi;
exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/geografia/unidadefederativa/spi/IUnidadeFederativaAplicacaoRepositorio.java
package com.example.exemplo.aplicacao.geografia.unidadefederativa.spi;

import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaConsulta;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaProjecao;

import dev.sauloaraujo.d5.framework.aplicacao.spi.IAgregadoAplicacaoRepositorio;

public interface IUnidadeFederativaAplicacaoRepositorio
        extends IAgregadoAplicacaoRepositorio<UnidadeFederativaProjecao, UnidadeFederativaConsulta> {
}

Serviço de aplicação

A pesquisa é encapsulada em um serviço de aplicação que estende AgregadoAplicacaoServicoAbstrato. Ele recebe pelo construtor o repositório de aplicação e o UnidadeFederativaServico, reaproveitado apenas para validar se o usuário está autorizado a buscar unidades federativas.

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/geografia/unidadefederativa/UnidadeFederativaAplicacaoServico.java
package com.example.exemplo.aplicacao.geografia.unidadefederativa;

import com.example.exemplo.aplicacao.geografia.unidadefederativa.spi.IUnidadeFederativaAplicacaoRepositorio;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaServico;

import dev.sauloaraujo.d5.framework.aplicacao.impl.AgregadoAplicacaoServicoAbstrato;
import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoAutorizacoes;

public final class UnidadeFederativaAplicacaoServico extends
        AgregadoAplicacaoServicoAbstrato<UnidadeFederativaCodigoIbge, UnidadeFederativa, AgregadoAutorizacoes<UnidadeFederativa>, UnidadeFederativaProjecao, UnidadeFederativaConsulta> {
    public UnidadeFederativaAplicacaoServico(IUnidadeFederativaAplicacaoRepositorio repositorio,
            UnidadeFederativaServico servico) {
        super(repositorio, servico);
    }
}

Infraestrutura

Entidade JPA

A persistência de unidade federativa é implementada no módulo exemplo-infraestrutura, no pacote com.example.exemplo.infraestrutura.geografia.unidadefederativa. Crie o package-info.java desse pacote, anotado com @NullMarked:

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/geografia/unidadefederativa/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;

O primeiro passo é a entidade JPA, que mapeia o agregado para a tabela UFI_UNIDADE_FEDERATIVA. Ela implementa IAgregadoJpa, parametrizada pelo agregado que representa, e declara as restrições das colunas com as anotações do Bean Validation, deixando na @Column apenas o que elas não expressam; veja Entidades JPA. Todas as colunas são obrigatórias (@NotNull), e o tamanho máximo de cada uma é declarado com @Size: o do identificador e o da sigla vêm das constantes dos objetos de valor correspondentes, e o do nome, que não é uma regra do domínio, mas um limite da tabela, é escrito diretamente. O Hibernate usa essas anotações para gerar as colunas e para validar a entidade antes de gravá-la — um nome com mais de 50 caracteres resulta em 422, e não em um erro do banco de dados. A coluna do identificador não pode ser alterada, e as de sigla e nome são únicas, para que o banco de dados rejeite as duplicidades que a validação do serviço não impede sob concorrência. A restrição do nome, porém, diferencia maiúsculas de minúsculas, ao contrário da validação do serviço: sob concorrência, ela impede apenas nomes idênticos.

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/geografia/unidadefederativa/UnidadeFederativaJpa.java
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;

import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;

import dev.sauloaraujo.d5.framework.infraestrutura.IAgregadoJpa;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

@Entity
@Table(name = "UFI_UNIDADE_FEDERATIVA")
public class UnidadeFederativaJpa implements IAgregadoJpa<UnidadeFederativa> {
    @Id
    @Column(name = "UFI_ID", updatable = false)
    @NotNull
    @Size(max = UnidadeFederativaCodigoIbge.TAMANHO)
    private String identificador;

    @Column(name = "UFI_SG", unique = true)
    @NotNull
    @Size(max = UnidadeFederativaSigla.TAMANHO)
    private String sigla;

    @Column(name = "UFI_NM", unique = true)
    @NotNull
    @Size(max = 50)
    private String nome;

    protected UnidadeFederativaJpa() {
    }

    public UnidadeFederativaJpa(String identificador, String sigla, String nome) {
        this.identificador = identificador;
        this.sigla = sigla;
        this.nome = nome;
    }

    public String getIdentificador() {
        return identificador;
    }

    public String getSigla() {
        return sigla;
    }

    public String getNome() {
        return nome;
    }
}

Repositório Spring Data

Em seguida, crie a interface Spring Data JPA que dá acesso à entidade. Além de estender JpaRepository, ela declara existePorSigla e existePorNome com consultas JPQL escritas explicitamente, já que essas verificações — que ignoram opcionalmente o identificador da própria unidade federativa — não são derivadas automaticamente do nome do método. existePorNome compara os nomes em minúsculas, de modo que "São Paulo" e "SÃO PAULO" sejam considerados o mesmo nome. Por fim, declara pesquisar, cuja consulta JPQL instancia a UnidadeFederativaProjecao diretamente através de uma expressão select new, filtrando pela sigla e pelo nome apenas quando o parâmetro correspondente não é nulo. Os filtros ignoram maiúsculas e minúsculas porque comparam a sigla e o nome em minúsculas com o padrão produzido por paraLike, já em minúsculas, declarando \ como caractere de escape.

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/geografia/unidadefederativa/IUnidadeFederativaRepositorioJpa.java
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;

import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaProjecao;

public interface IUnidadeFederativaRepositorioJpa extends JpaRepository<UnidadeFederativaJpa, String> {
    @Query("""
            select count(u) > 0
              from UnidadeFederativaJpa u
             where u.sigla = :sigla
               and (:identificadorIgnorado is null or u.identificador <> :identificadorIgnorado)
            """)
    boolean existePorSigla(String sigla, @Nullable String identificadorIgnorado);

    @Query("""
            select count(u) > 0
              from UnidadeFederativaJpa u
             where lower(u.nome) = lower(:nome)
               and (:identificadorIgnorado is null or u.identificador <> :identificadorIgnorado)
            """)
    boolean existePorNome(String nome, @Nullable String identificadorIgnorado);

    @Query("""
            select new com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaProjecao(u.identificador, u.sigla, u.nome)
              from UnidadeFederativaJpa u
             where (:sigla is null or lower(u.sigla) like :sigla escape '\\')
               and (:nome is null or lower(u.nome) like :nome escape '\\')
            """)
    Page<UnidadeFederativaProjecao> pesquisar(@Nullable String sigla, @Nullable String nome, Pageable pageable);
}

Implementação do repositório

Implemente IUnidadeFederativaRepositorio estendendo AgregadoRepositorioAbstrato, que já implementa incluir, buscarPorIdentificador, alterar, excluir e existePorIdentificador a partir dos métodos paraJpa, paraAgregado e paraIdentificadorJpa — os únicos que restam implementar. existePorSigla e existePorNome delegam para a interface Spring Data, convertendo o identificador a ignorar para o tipo usado pela entidade JPA.

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/geografia/unidadefederativa/UnidadeFederativaRepositorio.java
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;

import java.util.Objects;
import java.util.Optional;

import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaServico;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;
import com.example.exemplo.dominio.geografia.unidadefederativa.spi.IUnidadeFederativaRepositorio;

import dev.sauloaraujo.d5.framework.infraestrutura.AgregadoRepositorioAbstrato;

public class UnidadeFederativaRepositorio extends
        AgregadoRepositorioAbstrato<UnidadeFederativaCodigoIbge, UnidadeFederativa, String, UnidadeFederativaJpa>
        implements IUnidadeFederativaRepositorio {
    private final IUnidadeFederativaRepositorioJpa repositorioJpa;

    public UnidadeFederativaRepositorio(IUnidadeFederativaRepositorioJpa repositorioJpa) {
        super(repositorioJpa, UnidadeFederativaServico.IDENTIFICADOR_INEXISTENTE_MODELO);

        this.repositorioJpa = Objects.requireNonNull(repositorioJpa);
    }

    @Override
    protected UnidadeFederativaJpa paraJpa(UnidadeFederativa agregado) {
        Objects.requireNonNull(agregado);

        var identificador = agregado.getIdentificador().orElseThrow().toString();
        return new UnidadeFederativaJpa(identificador, agregado.getSigla().toString(), agregado.getNome());
    }

    @Override
    protected UnidadeFederativa paraAgregado(UnidadeFederativaJpa agregadoJpa) {
        Objects.requireNonNull(agregadoJpa);

        return new UnidadeFederativa(new UnidadeFederativaCodigoIbge(agregadoJpa.getIdentificador()),
                new UnidadeFederativaSigla(agregadoJpa.getSigla()), agregadoJpa.getNome());
    }

    @Override
    protected String paraIdentificadorJpa(UnidadeFederativaCodigoIbge identificador) {
        Objects.requireNonNull(identificador);

        return identificador.toString();
    }

    @Override
    public boolean existePorSigla(UnidadeFederativaSigla sigla,
            Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado) {
        Objects.requireNonNull(sigla);
        Objects.requireNonNull(identificadorIgnorado);

        var identificadorIgnoradoJpa = identificadorIgnorado.map(UnidadeFederativaCodigoIbge::toString).orElse(null);
        return repositorioJpa.existePorSigla(sigla.toString(), identificadorIgnoradoJpa);
    }

    @Override
    public boolean existePorNome(String nome, Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado) {
        Objects.requireNonNull(nome);
        Objects.requireNonNull(identificadorIgnorado);

        var identificadorIgnoradoJpa = identificadorIgnorado.map(UnidadeFederativaCodigoIbge::toString).orElse(null);
        return repositorioJpa.existePorNome(nome, identificadorIgnoradoJpa);
    }
}

Implementação do repositório de aplicação

Implemente também IUnidadeFederativaAplicacaoRepositorio estendendo AgregadoAplicacaoRepositorioAbstrato. Ela já implementa pesquisar(UnidadeFederativaConsulta), convertendo a consulta em paginação e ordenação do Spring Data — através de paraPropriedadeOrdenacao, que mapeia cada valor de UnidadeFederativaOrdenacao para a propriedade correspondente — e delegando a pesquisar(UnidadeFederativaConsulta, Pageable), que resta implementar: ele converte a sigla e o nome da consulta em padrões like com paraLike — em minúsculas, escapado e envolvido por %, ou null na ausência de filtro — e delega à interface Spring Data.

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/geografia/unidadefederativa/UnidadeFederativaAplicacaoRepositorio.java
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;

import java.util.Objects;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaConsulta;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaOrdenacao;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaProjecao;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.spi.IUnidadeFederativaAplicacaoRepositorio;

import dev.sauloaraujo.d5.framework.infraestrutura.AgregadoAplicacaoRepositorioAbstrato;

public class UnidadeFederativaAplicacaoRepositorio extends
        AgregadoAplicacaoRepositorioAbstrato<UnidadeFederativaProjecao, UnidadeFederativaOrdenacao, UnidadeFederativaConsulta>
        implements IUnidadeFederativaAplicacaoRepositorio {
    private final IUnidadeFederativaRepositorioJpa repositorioJpa;

    public UnidadeFederativaAplicacaoRepositorio(IUnidadeFederativaRepositorioJpa repositorioJpa) {
        this.repositorioJpa = Objects.requireNonNull(repositorioJpa);
    }

    @Override
    protected Page<UnidadeFederativaProjecao> pesquisar(UnidadeFederativaConsulta consulta, Pageable pageable) {
        Objects.requireNonNull(consulta);
        Objects.requireNonNull(pageable);

        var sigla = paraLike(consulta.getSigla());
        var nome = paraLike(consulta.getNome());
        return repositorioJpa.pesquisar(sigla, nome, pageable);
    }

    @Override
    protected String paraPropriedadeOrdenacao(UnidadeFederativaOrdenacao ordenacao) {
        Objects.requireNonNull(ordenacao);

        return switch (ordenacao) {
        case IDENTIFICADOR -> "identificador";
        case SIGLA -> "sigla";
        case NOME -> "nome";
        };
    }
}

Testes do repositório

Diferente do serviço, cujo comportamento é especificado com cenários em Gherkin sobre dublês, os repositórios são testados por integração contra um banco H2 embarcado, usando @DataJpaTest. Esse tipo de teste exige uma classe de configuração do Spring Boot no classpath de teste — o gerador já criou uma mínima, anotada com @SpringBootApplication:

exemplo/infraestrutura/src/test/java/com/example/exemplo/infraestrutura/InfraestruturaTesteAplicacao.java
package com.example.exemplo.infraestrutura;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class InfraestruturaTesteAplicacao {
}

Escreva os testes cobrindo as operações do repositório; as duas variações de existePorSigla e existePorNome — considerando todas as unidades federativas e ignorando o identificador de uma delas —; e a comparação de nomes que ignora maiúsculas e minúsculas.

exemplo/infraestrutura/src/test/java/com/example/exemplo/infraestrutura/geografia/unidadefederativa/UnidadeFederativaRepositorioTest.java
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;

import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;

import dev.sauloaraujo.d5.framework.dominio.AgregadoInexistenteExcecao;

@DataJpaTest
class UnidadeFederativaRepositorioTest {
    @Autowired
    private IUnidadeFederativaRepositorioJpa repositorioJpa;

    private UnidadeFederativaRepositorio repositorio;

    @BeforeEach
    void configurar() {
        repositorio = new UnidadeFederativaRepositorio(repositorioJpa);
    }

    private static UnidadeFederativa criar(String codigoIbge, String sigla, String nome) {
        return new UnidadeFederativa(new UnidadeFederativaCodigoIbge(codigoIbge), new UnidadeFederativaSigla(sigla),
                nome);
    }

    @Test
    void incluiEBuscaPorIdentificador() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        var unidadeFederativa = repositorio.buscarPorIdentificador(new UnidadeFederativaCodigoIbge("35"));

        assertEquals(new UnidadeFederativaSigla("SP"), unidadeFederativa.getSigla());
        assertEquals("São Paulo", unidadeFederativa.getNome());
    }

    @Test
    void buscaPorIdentificadorInexistenteLancaExcecao() {
        assertThrows(AgregadoInexistenteExcecao.class,
                () -> repositorio.buscarPorIdentificador(new UnidadeFederativaCodigoIbge("35")));
    }

    @Test
    void existePorIdentificador() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        assertTrue(repositorio.existePorIdentificador(new UnidadeFederativaCodigoIbge("35")));
        assertFalse(repositorio.existePorIdentificador(new UnidadeFederativaCodigoIbge("33")));
    }

    @Test
    void altera() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        repositorio.alterar(criar("35", "SP", "São Paulo Alterado"));

        var unidadeFederativa = repositorio.buscarPorIdentificador(new UnidadeFederativaCodigoIbge("35"));
        assertEquals("São Paulo Alterado", unidadeFederativa.getNome());
    }

    @Test
    void exclui() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        repositorio.excluir(new UnidadeFederativaCodigoIbge("35"));

        assertFalse(repositorio.existePorIdentificador(new UnidadeFederativaCodigoIbge("35")));
    }

    @Test
    void existePorSiglaConsiderandoTodasAsUnidadesFederativas() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        assertTrue(repositorio.existePorSigla(new UnidadeFederativaSigla("SP"), Optional.empty()));
        assertFalse(repositorio.existePorSigla(new UnidadeFederativaSigla("RJ"), Optional.empty()));
    }

    @Test
    void existePorSiglaIgnorandoOProprioIdentificador() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        var identificador = Optional.of(new UnidadeFederativaCodigoIbge("35"));

        assertFalse(repositorio.existePorSigla(new UnidadeFederativaSigla("SP"), identificador));
    }

    @Test
    void existePorSiglaNaoIgnorandoUmaUnidadeFederativaDiferente() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));
        repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));

        var identificador = Optional.of(new UnidadeFederativaCodigoIbge("33"));

        assertTrue(repositorio.existePorSigla(new UnidadeFederativaSigla("SP"), identificador));
    }

    @Test
    void existePorNomeConsiderandoTodasAsUnidadesFederativas() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        assertTrue(repositorio.existePorNome("São Paulo", Optional.empty()));
        assertFalse(repositorio.existePorNome("Rio de Janeiro", Optional.empty()));
    }

    @Test
    void existePorNomeIgnorandoOProprioIdentificador() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        var identificador = Optional.of(new UnidadeFederativaCodigoIbge("35"));

        assertFalse(repositorio.existePorNome("São Paulo", identificador));
    }

    @Test
    void existePorNomeNaoIgnorandoUmaUnidadeFederativaDiferente() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));
        repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));

        var identificador = Optional.of(new UnidadeFederativaCodigoIbge("33"));

        assertTrue(repositorio.existePorNome("São Paulo", identificador));
    }

    @Test
    void existePorNomeIgnorandoMaiusculasEMinusculas() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        assertTrue(repositorio.existePorNome("SÃO PAULO", Optional.empty()));
    }
}

Escreva também os testes do repositório de aplicação, cobrindo a pesquisa sem filtro, sem resultados, filtrando por um trecho do nome e da sigla e ordenando pela sigla.

exemplo/infraestrutura/src/test/java/com/example/exemplo/infraestrutura/geografia/unidadefederativa/UnidadeFederativaAplicacaoRepositorioTest.java
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;

import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaConsulta;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaOrdenacao;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;

import dev.sauloaraujo.d5.framework.infraestrutura.AgregadoRepositorioAbstrato;

@DataJpaTest
class UnidadeFederativaAplicacaoRepositorioTest {
    @Autowired
    private IUnidadeFederativaRepositorioJpa repositorioJpa;

    private AgregadoRepositorioAbstrato<UnidadeFederativaCodigoIbge, UnidadeFederativa, String, UnidadeFederativaJpa> repositorio;

    private UnidadeFederativaAplicacaoRepositorio repositorioAplicacao;

    @BeforeEach
    void configurar() {
        repositorio = new UnidadeFederativaRepositorio(repositorioJpa);
        repositorioAplicacao = new UnidadeFederativaAplicacaoRepositorio(repositorioJpa);
    }

    private static UnidadeFederativa criar(String codigoIbge, String sigla, String nome) {
        return new UnidadeFederativa(new UnidadeFederativaCodigoIbge(codigoIbge), new UnidadeFederativaSigla(sigla),
                nome);
    }

    private static UnidadeFederativaConsulta consultar() {
        return consultar(UnidadeFederativaOrdenacao.NOME, Optional.empty(), Optional.empty());
    }

    private static UnidadeFederativaConsulta consultar(UnidadeFederativaOrdenacao ordenacao, Optional<String> sigla,
            Optional<String> nome) {
        return new UnidadeFederativaConsulta(Optional.of(ordenacao), Optional.empty(), Optional.empty(),
                Optional.empty(), sigla, nome);
    }

    @Test
    void pesquisaRetornaProjecaoComIdentificadorSiglaENome() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));
        repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));

        var resultado = repositorioAplicacao.pesquisar(consultar());

        assertEquals(2, resultado.getTotal());
        assertEquals(2, resultado.getProjecoes().size());

        var primeira = resultado.getProjecoes().get(0);
        assertEquals(new UnidadeFederativaCodigoIbge("33"), primeira.getIdentificador());
        assertEquals(new UnidadeFederativaSigla("RJ"), primeira.getSigla());
        assertEquals("Rio de Janeiro", primeira.getNome());

        var segunda = resultado.getProjecoes().get(1);
        assertEquals(new UnidadeFederativaCodigoIbge("35"), segunda.getIdentificador());
        assertEquals(new UnidadeFederativaSigla("SP"), segunda.getSigla());
        assertEquals("São Paulo", segunda.getNome());
    }

    @Test
    void pesquisaSemUnidadesFederativasRetornaResultadoVazio() {
        var resultado = repositorioAplicacao.pesquisar(consultar());

        assertEquals(0, resultado.getTotal());
        assertEquals(0, resultado.getProjecoes().size());
    }

    @Test
    void pesquisaFiltrandoPorTrechoDoNome() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));
        repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));

        var resultado = repositorioAplicacao
                .pesquisar(consultar(UnidadeFederativaOrdenacao.NOME, Optional.empty(), Optional.of("paulo")));

        assertEquals(1, resultado.getTotal());
        assertEquals("São Paulo", resultado.getProjecoes().get(0).getNome());
    }

    @Test
    void pesquisaFiltrandoPorTrechoDaSigla() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));
        repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));

        var resultado = repositorioAplicacao
                .pesquisar(consultar(UnidadeFederativaOrdenacao.NOME, Optional.of("r"), Optional.empty()));

        assertEquals(1, resultado.getTotal());
        assertEquals("Rio de Janeiro", resultado.getProjecoes().get(0).getNome());
    }

    @Test
    void pesquisaOrdenadaPorSigla() {
        repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));
        repositorio.incluir(criar("12", "AC", "Acre"));
        repositorio.incluir(criar("27", "AL", "Alagoas"));

        var resultado = repositorioAplicacao
                .pesquisar(consultar(UnidadeFederativaOrdenacao.SIGLA, Optional.empty(), Optional.empty()));

        assertEquals(new UnidadeFederativaSigla("AC"), resultado.getProjecoes().get(0).getSigla());
        assertEquals(new UnidadeFederativaSigla("AL"), resultado.getProjecoes().get(1).getSigla());
        assertEquals(new UnidadeFederativaSigla("RJ"), resultado.getProjecoes().get(2).getSigla());
    }
}

Execute os testes da infraestrutura — com -am, o Maven executa também os cenários do domínio:

mvn -pl infraestrutura -am test

UI

Unidade federativa é exposta como uma API REST no módulo exemplo-ui-backend, pacote com.example.exemplo.ui.backend.geografia.unidadefederativa. Crie o package-info.java desse pacote, anotado com @NullMarked:

exemplo/ui-backend/src/main/java/com/example/exemplo/ui/backend/geografia/unidadefederativa/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.ui.backend.geografia.unidadefederativa;

Controlador

O controlador estende AgregadoControladorAbstrato, parametrizado pelo identificador, agregado, autorizações, projeção e consulta de unidade federativa, e recebe pelo construtor o UnidadeFederativaServico e o UnidadeFederativaAplicacaoServico. O @RequestMapping combina o prefixo Backend.PREFIXO com o caminho geografia/unidade-federativa. O parâmetro identificador dos endpoints é recebido diretamente como UnidadeFederativaCodigoIbge graças à serialização de objetos de valor, e as operações de escrita são executadas em uma transação graças ao gerenciamento de transações — que é também o motivo de o controlador, ao contrário das classes do domínio e da aplicação, não ser final.

exemplo/ui-backend/src/main/java/com/example/exemplo/ui/backend/geografia/unidadefederativa/UnidadeFederativaControlador.java
package com.example.exemplo.ui.backend.geografia.unidadefederativa;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaAplicacaoServico;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaConsulta;
import com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaProjecao;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaServico;

import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoAutorizacoes;
import dev.sauloaraujo.d5.framework.ui.AgregadoControladorAbstrato;
import dev.sauloaraujo.d5.framework.ui.Backend;

@RestController
@RequestMapping(Backend.PREFIXO + "geografia/unidade-federativa")
public class UnidadeFederativaControlador extends
        AgregadoControladorAbstrato<UnidadeFederativaCodigoIbge, UnidadeFederativa, AgregadoAutorizacoes<UnidadeFederativa>, UnidadeFederativaProjecao, UnidadeFederativaConsulta> {
    public UnidadeFederativaControlador(UnidadeFederativaServico servico,
            UnidadeFederativaAplicacaoServico servicoAplicacao) {
        super(servico, servicoAplicacao);
    }
}

Segurança

O gerador criou uma configuração de segurança que restringe o backend a usuários autenticados, no pacote com.example.exemplo.ui.backend.seguranca, que estende SegurancaConfiguracaoAbstrata: ela já protege /backend/** com autenticação HTTP Basic e CSRF baseado em cookie, restando declarar, em autorizar, quais endpoints liberar — sem nenhuma regra, todo endpoint sob /backend/** seria negado; aqui, todos passam a exigir apenas autenticação — e expor os usuários disponíveis através de um UserDetailsService. Ela declara dois usuários em memória: a/a, com a autorização ADMINISTRADOR, e u/u, com USUARIO. O pacote tem um package-info.java, anotado com @NullMarked:

exemplo/ui-backend/src/main/java/com/example/exemplo/ui/backend/seguranca/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.ui.backend.seguranca;
exemplo/ui-backend/src/main/java/com/example/exemplo/ui/backend/seguranca/SegurancaConfiguracao.java
package com.example.exemplo.ui.backend.seguranca;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

import com.example.exemplo.dominio.Autorizacao;

import dev.sauloaraujo.d5.framework.ui.seguranca.SegurancaConfiguracaoAbstrata;

@Configuration
public class SegurancaConfiguracao extends SegurancaConfiguracaoAbstrata {
    @Override
    protected void autorizar(
            AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry autorizacao) {
        autorizacao.requestMatchers(BACKEND).authenticated();
    }

    @Bean
    UserDetailsService usuarios(PasswordEncoder codificadorSenha) {
        var administrador = User.withUsername("a").password(codificadorSenha.encode("a"))
                .authorities(Autorizacao.ADMINISTRADOR.name()).build();

        var usuario = User.withUsername("u").password(codificadorSenha.encode("u"))
                .authorities(Autorizacao.USUARIO.name()).build();

        return new InMemoryUserDetailsManager(administrador, usuario);
    }
}

Autenticação

Para o cliente descobrir o usuário autenticado e o que ele pode fazer, o gerador criou um controlador que estende AutenticacaoControladorAbstrato, expondo o recurso de autenticação em Backend.PREFIXO + "autenticacao", e o próprio recurso, AutenticacaoRecurso, que estende AutenticacaoRecursoAbstrato. Gerados, eles trazem apenas o nome do usuário. Altere o recurso, acrescentando o atributo unidadeFederativa, com as autorizações do usuário sobre o agregado, e altere o controlador, para recebê-las do UnidadeFederativaServico: criarAutorizacoes é chamado sem identificador, pois essas autorizações valem para o agregado como um todo. Ao acrescentar outros agregados, repita o atributo para cada um que o cliente precisar conhecer, por exemplo, para exibir ou ocultar as opções de menu.

exemplo/ui-backend/src/main/java/com/example/exemplo/ui/backend/seguranca/AutenticacaoRecurso.java
package com.example.exemplo.ui.backend.seguranca;

import java.util.Objects;

import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;

import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoAutorizacoes;
import dev.sauloaraujo.d5.framework.ui.seguranca.AutenticacaoRecursoAbstrato;

public class AutenticacaoRecurso extends AutenticacaoRecursoAbstrato {
    private final AgregadoAutorizacoes<UnidadeFederativa> unidadeFederativa;

    public AutenticacaoRecurso(String usuario, AgregadoAutorizacoes<UnidadeFederativa> unidadeFederativa) {
        super(usuario);
        this.unidadeFederativa = Objects.requireNonNull(unidadeFederativa);
    }

    public AgregadoAutorizacoes<UnidadeFederativa> getUnidadeFederativa() {
        return unidadeFederativa;
    }
}
exemplo/ui-backend/src/main/java/com/example/exemplo/ui/backend/seguranca/AutenticacaoControlador.java
package com.example.exemplo.ui.backend.seguranca;

import java.util.Objects;
import java.util.Optional;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaServico;

import dev.sauloaraujo.d5.framework.dominio.spi.ISegurancaServico;
import dev.sauloaraujo.d5.framework.ui.Backend;
import dev.sauloaraujo.d5.framework.ui.seguranca.AutenticacaoControladorAbstrato;

@RestController
@RequestMapping(Backend.PREFIXO + "autenticacao")
public class AutenticacaoControlador extends AutenticacaoControladorAbstrato {
    private final UnidadeFederativaServico unidadeFederativaServico;

    public AutenticacaoControlador(ISegurancaServico segurancaServico,
            UnidadeFederativaServico unidadeFederativaServico) {
        super(segurancaServico);
        this.unidadeFederativaServico = Objects.requireNonNull(unidadeFederativaServico);
    }

    @Override
    protected AutenticacaoRecurso recurso() {
        var unidadeFederativa = unidadeFederativaServico.criarAutorizacoes(Optional.empty());
        return new AutenticacaoRecurso(segurancaServico.getUsuario(), unidadeFederativa);
    }
}

O JSON devolvido tem, além de usuario, o objeto unidadeFederativa com incluir, buscar, alterar e excluir.

Aplicação Spring Boot

Por fim, o gerador criou a classe com o main que sobe a aplicação, anotada com @SpringBootApplication, no pacote raiz com.example.exemplo. É a partir do pacote dela que o component scan encontra os controladores e a configuração de segurança, e que o registro de beans encontra os serviços e as implementações de repositório de todos os subdomínios. O pacote tem um package-info.java, anotado com @NullMarked:

exemplo/ui-backend/src/main/java/com/example/exemplo/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo;
exemplo/ui-backend/src/main/java/com/example/exemplo/BackendAplicacao.java
package com.example.exemplo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BackendAplicacao {
    public static void main(String[] args) {
        SpringApplication.run(BackendAplicacao.class, args);
    }
}

Testando com o Swagger UI

Suba BackendAplicacao pela IDE ou, no diretório exemplo, executando:

mvn install
mvn -f ui-backend spring-boot:run

O mvn install é necessário porque spring-boot:run, executado apenas no módulo ui-backend, resolve os demais módulos do projeto a partir do repositório Maven local. A aplicação usa um banco H2 em memória, trazido por d5-framework-infraestrutura, cujas tabelas são criadas a partir das entidades JPA a cada inicialização — portanto, os dados incluídos se perdem quando a aplicação é encerrada.

Acesse http://localhost:8080/swagger-ui/index.html. Como d5-framework-springdoc está no classpath, o Swagger UI é gerado automaticamente a partir dos controladores do backend, listados sob a tag unidade-federativa-controlador:

Visão geral do Swagger UI, listando as operações de UnidadeFederativaControlador

Como todo endpoint sob /backend/** exige um usuário autenticado, e a aplicação não desafia o navegador com o diálogo nativo de autenticação básica, é preciso autenticar pelo próprio Swagger UI: clique em Authorize, no canto superior direito, e informe um dos usuários criados na configuração de segurança — a/a, administrador, ou u/u, usuário comum.

Diálogo de autenticação HTTP Basic do Swagger UI

A partir daí, toda requisição disparada pelo Swagger UI leva o cabeçalho Authorization com essas credenciais e, junto, o cabeçalho X-XSRF-TOKEN, com o token de CSRF exigido pelas operações de escrita. O Swagger UI lê esse token, a cada requisição, do cookie XSRF-TOKEN, que o servidor envia já ao carregar a página; veja CSRF no Swagger UI. A primeira requisição autenticada, porém, renova o token: sua resposta apaga o cookie, e só a resposta seguinte traz um novo. Por isso, uma operação de escrita executada logo depois da primeira requisição autenticada vai sem o token e responde 403 com a mensagem Token CSRF ausente ou inválido. — basta executá-la de novo, já com o novo cookie.

Com a sessão autenticada, expanda cada operação e use Try it out:

Incluir

GET /backend/geografia/unidade-federativa/inclusao não recebe parâmetros e devolve um recurso vazio, usado apenas para montar o formulário de inclusão. POST /backend/geografia/unidade-federativa/incluir recebe a unidade federativa no corpo:

{
  "identificador": "35",
  "sigla": "SP",
  "nome": "São Paulo"
}

O código IBGE e a sigla trafegam como strings simples graças à serialização de objetos de valor.

Corpo da requisição de inclusão preenchido com a unidade federativa de São Paulo

Resposta 200 da inclusão

Pesquisar

GET /backend/geografia/unidade-federativa/pesquisa recebe os campos de UnidadeFederativaConsulta — sigla e nome (filtros parciais, opcionais), ordenacao, descendente, indicePagina e tamanhoPagina — como parâmetros de consulta individuais, e o Swagger UI, graças à expansão da consulta feita pelo d5-framework-springdoc, exibe cada um como um campo separado do formulário. Deixe em branco os campos que não quiser informar — eles assumem os padrões declarados em Consulta; deixe todos em branco para listar a primeira página, sem filtro. Valores inválidos, como tamanhoPagina igual a 0, resultam em 422, com a mensagem da validação; veja ExcecaoManipulador.

Parâmetro nome preenchido com Paulo na operação de pesquisa

Resposta 200 da pesquisa, filtrada por nome, com a lista de projeções e o total

Alterar

GET /backend/geografia/unidade-federativa/{identificador}/alteracao recebe o código IBGE no parâmetro de caminho identificador — por exemplo, 35 — e devolve o agregado e as autorizações do usuário atual sobre ele:

Parâmetro identificador preenchido com 35 na operação de alteração

Resposta 200 da alteração, com o agregado e as autorizações do usuário

POST /backend/geografia/unidade-federativa/{identificador}/alterar recebe o mesmo identificador no caminho e a unidade federativa alterada no corpo, repetindo o identificador:

{
  "identificador": "35",
  "sigla": "SP",
  "nome": "São Paulo (alterado)"
}

Corpo da requisição de alteração com o nome atualizado

Resposta 200 da alteração

Excluir

GET /backend/geografia/unidade-federativa/{identificador}/exclusao recebe o mesmo parâmetro identificador e devolve novamente o agregado e as autorizações, desta vez para confirmar a exclusão:

Parâmetro identificador preenchido com 35 na operação de exclusão

Resposta 200 da exclusão, com a unidade federativa já alterada

POST /backend/geografia/unidade-federativa/{identificador}/excluir recebe apenas o identificador no caminho, sem corpo:

Parâmetro identificador preenchido com 35 na operação de exclusão definitiva

Resposta 200 da exclusão definitiva

Para trocar de usuário, clique em Authorize, em seguida em Logout e autentique-se novamente, desta vez como u/u. Como o usuário u não é administrador, repetir as operações de escrita autenticado como u deve falhar de acordo com as autorizações definidas em UnidadeFederativaServico — a pesquisa e os formulários de alteração e de exclusão, que dependem apenas da autorização de busca, são permitidos a qualquer usuário autenticado, mas as operações de escrita respondem 403.

Na lição 2, você vai acrescentar ao subdomínio Geografia o município, que referencia a unidade federativa.