Spring Boot

Estrutura e prática Spring Boot framework

Resumo em construção
Voltar

Material complementar:

  • Conteúdo no YouTube: Acesse
  • Conteúdo no GitHub: Acesse
  • Curso com certificado na Workover Academy (Criar API REST Spring Boot com Kotlin): Acesse

Conceito:

Camada que atua sob framework Spring, simplificando-o e modernizando-o, seguindo padrão MVC. Spring implementa padrão IoC (Inversion of Control), onde os objetos (Spring Beans / Beans) são instanciados automaticamente pelo container do Spring, dispensando criação manual.


Pré-requisitos (Windows):

JDK (Java Development Kit) é kit de ferramentas e bibliotecas para desenvolver aplicações Java. Gradle é automatizador de build moderno que usa linguagem baseada em Groovy ou Kotlin para configurar projetos.

  1. JDK: Acesse
    1. Baixar e instalar JDK Oracle
    2. Win + R, digite "SystemPropertiesAdvanced", acessar Variáveis de Ambiente
    3. Aba "Variáveis do sistema", em Novo, criar/editar variáveis:
        Criar:
      • Nome: JAVA_HOME
      • Valor: Caminho da pasta com JDK instalado (Ex: "C:\Program Files\Java\jdk-24")
      • Confirmar criação OK

      • Editar:
      • Nome: Path
      • Clicar em Novo
      • Valor: %JAVA_HOME%\bin
      • Confirmar criação OK
    4. Verificar instalação no CMD, com comando "java -version"
  2. Kotlin compiler: Acesse
    1. Baixar e instalar Kotlin compiler (.zip)
    2. Diretório "C:/", crie pasta "Kotlin", e descompacte o Kotlin dentro dela
    3. Win + R, digite "SystemPropertiesAdvanced", acessar Variáveis de Ambiente
    4. Aba "Variáveis do sistema", editar variável Path: Editar:
      • Nome: Path
      • Clicar em Novo
      • Valor: C:\Kotlin\kotlinc\bin
      • Confirmar criação OK
    5. Verificar instalação: Win + R, digitar CMD, e informar comando "kotlinc -version"
  3. Gradle: Acesse
    1. Baixar e instalar Gradle: site oficial Gradle, Install, Install manually, Step 1, baixar Binary-only
    2. Diretório "C:/", crie pasta "Gradle", e descompacte o Gradle dentro dela
    3. Win + R, digite "SystemPropertiesAdvanced", acessar Variáveis de Ambiente
    4. Aba "Variáveis do sistema", editar variável Path: Editar:
      • Nome: Path
      • Clicar em Novo
      • Valor: C:\Gradle\gradle-VERSAO\bin
      • Confirmar criação OK
    5. Verificar instalação: Win + R, digitar CMD, e informar comando "gradle -v"

Criar projeto:

Em 'start.spring.io', selecione:

  1. Project: Gradle - Kotlin
  2. Language: Kotlin
  3. Spring Boot: 3.5.13
  4. Group: com.seuprojeto
  5. Artifact: nomeProjeto
  6. Package name: com.seuprojeto.nomeProjeto
  7. Packaging: Jar
  8. Configuration: YAML
  9. Java: 21
  10. Dependencies:
    • Spring Web
    • Spring Data JPA
    • Validation
    • Spring Boot Dev Tools
  11. Generate (extrair zip e acessar projeto com IntelliJ)

Layer-based Structure:

Estrutura tradicional/standard (Structure/Package by Layer), onde cada camada (Controller, Service, Repository) é organizada em pacotes separados. Melhor cenário: sistemas mais simples, bem estruturados, com responsabilidades claras (como em CRUDs). Pior cenário: pouca flexibilidade para mudanças, acoplamento entre camadas e dificuldade em evoluir o domínio:


com.company.project
├── ProjectApplication.java
├── config
│   ├── SecurityConfig.java
│   ├── DatabaseConfig.java
│   └── SwaggerConfig.java
├── controller
│   ├── UserController.java
│   └── ProductController.java
├── service
│   ├── UserService.java
│   ├── UserServiceImpl.java
│   ├── ProductService.java
│   └── ProductServiceImpl.java
├── repository
│   ├── UserRepository.java
│   └── ProductRepository.java
├── model
│   ├── entity
│   │   ├── User.java
│   │   └── Product.java
│   └── dto
│       ├── UserDTO.java
│       ├── CreateUserRequest.java
│       └── ProductDTO.java
├── exception
│   ├── GlobalExceptionHandler.java
│   ├── ResourceNotFoundException.java
│   └── ValidationException.java
├── mapper
│   ├── UserMapper.java
│   └── ProductMapper.java
├── security
│   ├── JwtTokenProvider.java
│   └── CustomUserDetailsService.java
└── util
    ├── DateUtil.java
    └── ValidationUtil.java

src/main/resources:


resources
├── application.yml
├── application-dev.yml
├── application-prod.yml
├── static
│   └── (static files)
├── templates
│   └── (template files)
└── db
    └── migration
        ├── V1__Create_users_table.sql
        └── V2__Add_products_table.sql

src/test:


src/test/java/com/company/project
├── controller
│   └── UserControllerTest.java
├── service
│   └── UserServiceTest.java
└── repository
    └── UserRepositoryTest.java

Layer-based Structure com módulos em pacotes:

Arquitetura anterior, onde projeto é organizado em módulos via pacotes, que seguem a estrutura de camadas tradicional (controller/seevice/repository/model):


com.whateverer.config
 ├── SwaggerConfig.kt
 ├── SecurityConfig.kt
 ├── JwtConfig.kt
 ├── OpenApiConfig.kt
 ├── JacksonConfig.kt
 ├── CorsConfig.kt
 ├── DatabaseConfig.kt
 ├── CacheConfig.kt
 └── WebMvcConfig.kt

com.whateverer.feature.account
 ├── controller
 │     ├── AccountController.kt
 │     ├── AccountAdminController.kt
 │     └── AccountAuthController.kt
 │
 ├── model
 │     ├── dto
 │     │     ├── CreateAccountRequest.kt
 │     │     ├── UpdateAccountRequest.kt
 │     │     ├── AccountResponse.kt
 │     │     ├── AccountSummaryResponse.kt
 │     │     └── AccountLoginRequest.kt
 │     │
 │     └── entity
 │           ├── AccountEntity.kt
 │           ├── AccountProfileEntity.kt
 │           ├── AccountRoleEntity.kt
 │           └── AccountStatus.kt
 │
 ├── repository
 │     ├── AccountRepository.kt
 │     ├── AccountQueryRepository.kt
 │     └── AccountCustomRepository.kt
 │
 ├── service
 │     ├── AccountService.kt
 │     ├── CreateAccountService.kt
 │     ├── UpdateAccountService.kt
 │     ├── DeleteAccountService.kt
 │     ├── GetAccountService.kt
 │     ├── ListAccountService.kt
 │     ├── AccountValidationService.kt
 │     └── AccountAuthService.kt
 │
 └── utils
       ├── AccountMapper.kt
       ├── AccountUtils.kt
       ├── AccountValidator.kt
       ├── AccountConstants.kt
       └── AccountPermissions.kt

com.whateverer.feature.payment
 ├── controller
 │     ├── PaymentController.kt
 │     ├── RefundController.kt
 │     └── PaymentWebhookController.kt
 │
 ├── model
 │     ├── dto
 │     │     ├── CreatePaymentRequest.kt
 │     │     ├── PaymentResponse.kt
 │     │     ├── RefundRequest.kt
 │     │     ├── PaymentStatusResponse.kt
 │     │     └── InstallmentSimulationResponse.kt
 │     │
 │     └── entity
 │           ├── PaymentEntity.kt
 │           ├── RefundEntity.kt
 │           ├── PaymentMethodEntity.kt
 │           └── PaymentStatus.kt
 │
 ├── repository
 │     ├── PaymentRepository.kt
 │     ├── RefundRepository.kt
 │     └── PaymentQueryRepository.kt
 │
 ├── service
 │     ├── PaymentService.kt
 │     ├── RefundService.kt
 │     ├── PaymentGatewayService.kt
 │     ├── PaymentValidationService.kt
 │     ├── InstallmentService.kt
 │     └── PaymentWebhookService.kt
 │
 └── utils
       ├── PaymentMapper.kt
       ├── PaymentUtils.kt
       ├── PaymentValidator.kt
       ├── PaymentConstants.kt
       └── InstallmentCalculator.kt

Feature-based Structure:

Estrutura moderna (Structure/Package by Feature), onde cada feature/recurso é organizada em pacotes separados, sendo ideal para projetos complexos, escaláveis e microservices. Melhor cenário: sistemas grandes, com várias funcionalidades, onde cada módulo agrupa lógica, testes e regras relacionadas. Pior cenário: pode gerar duplicação de código se não for bem estruturado, e o time precisa se organizar para manter o alinhamento entre features:


com.myapp.project
 ├── Application.java
 ├── user/
 │     ├── UserController.java
 │     ├── UserService.java
 │     ├── UserRepository.java
 │     ├── User.java (entity)
 │     ├── UserDto.java
 ├── product/
 │     ├── ProductController.java
 │     ├── ProductService.java
 │     ├── ProductRepository.java
 │     ├── Product.java
 │     ├── ProductDto.java
 ├── auth/
 │     ├── AuthController.java
 │     ├── AuthService.java
 │     ├── JwtService.java
 │     ├── JwtAuthenticationFilter.java
 │     ├── CustomUserDetailsService.java
 │     ├── LoginRequest.java
 │     ├── RegisterRequest.java
 │     ├── AuthResponse.java
 │     └── JwtUtil.java
 ├── config/
 │     ├── AppConfig.java
 │     ├── SecurityConfig.java
 │     ├── DatabaseConfig.java
 │     ├── SwaggerConfig.java
 │     └── JwtConfig.java
 ├── exception/
 │     ├── GlobalExceptionHandler.java
 │     ├── ResourceNotFoundException.java
 │     ├── ValidationException.java
 │     ├── UnauthorizedException.java
 │     ├── InvalidTokenException.java
 │     └── TokenExpiredException.java
 ├── mapper/
 │     ├── UserMapper.java
 │     └── ProductMapper.java
 └── util/
       ├── DateUtil.java
       ├── ValidationUtil.java
       └── PasswordUtil.java

src/main/resources:


resources
├── application.yml
├── application-dev.yml
├── application-prod.yml
├── static
│   └── (static files, css, js, images)
├── templates
│   └── (template files, html, thymeleaf)
├── config
│   ├── security.yml
│   ├── swagger.yml
│   ├── e.g. application.yml
│   ├── logging.yml
│   └── database.ym
└── db
    └── migration
        ├── V1__Create_users_table.sql
        └── V2__Add_products_table.sql

src/test:


src/test/java/com/myapp/project
├── user/
│     ├── UserControllerTest.java
│     ├── UserServiceTest.java
│     └── UserRepositoryTest.java
├── product/
│     ├── ProductControllerTest.java
│     ├── ProductServiceTest.java
│     └── ProductRepositoryTest.java
└── config/
    └── SecurityConfigTest.java

Hexagonal Architecture:

Arquitetura moderna (Hexagonal Architecture / Ports and Adapters), onde a aplicação é organizada em camadas concêntricas, com o domínio central isolado de detalhes de infraestrutura, sendo ideal para projetos complexos, escaláveis e microservices. Melhor cenário: sistemas complexos, com alto desacoplamento, onde o domínio é protegido de detalhes externos, permitindo fácil troca de tecnologia e alta testabilidade. Pior cenário: curva de aprendizado mais alta, abstrações adicionais, exigindo mais esforço para implementar no início:


src/main/java/com/myapp/project
├── application/
│   ├── port/
│   │   ├── input/
│   │   │   ├── UserUseCase.java
│   │   │   └── ProductUseCase.java
│   │   └── output/
│   │       ├── UserPersistencePort.java
│   │       └── ProductPersistencePort.java
│   ├── service/
│   │   ├── UserServiceImpl.java
│   │   └── ProductServiceImpl.java
│   ├── dto/
│   │   ├── UserDto.java
│   │   └── ProductDto.java
│   └── mapper/
│       ├── UserMapper.java
│       └── ProductMapper.java
├── domain/
│   ├── model/
│   │   ├── User.java
│   │   └── Product.java
│   ├── exception/
│   │   ├── ResourceNotFoundException.java
│   │   ├── ValidationException.java
│   │   ├── UnauthorizedException.java
│   │   ├── InvalidTokenException.java
│   │   └── TokenExpiredException.java
│   └── vo/
│       └── (Value Objects opcionais)
├── infrastructure/
│   ├── input/
│   │   ├── rest/
│   │   │   ├── UserController.java
│   │   │   └── ProductController.java
│   │   └── config/
│   │       ├── SwaggerConfig.java
│   │       └── SecurityConfig.java
│   └── output/
│       ├── persistence/
│       │   ├── UserRepositoryImpl.java
│       │   └── ProductRepositoryImpl.java
│       ├── security/
│       │   ├── JwtTokenProvider.java
│       │   └── CustomUserDetailsService.java
│       └── config/
│           └── DatabaseConfig.java
├── shared/
│   └── exception/
│       └── GlobalExceptionHandler.java
└── Application.java

Modular Monolith + Clean Architecture + DDD:

Arquitetura profissional moderna (Modular Monolith + Clean Architecture + DDD), onde aplicação é organizada em módulos independentes, seguindo princípios de Clean Architecture e Domain-Driven Design, sendo ideal para projetos complexos, escaláveis e microservices. Melhor cenário: sistemas complexos, com alto desacoplamento, onde o domínio é protegido de detalhes externos, permitindo fácil evolução do domínio e alta testabilidade. Pior cenário: curva de aprendizado mais alta, abstrações adicionais, exigindo mais esforço para implementar no início:


my-erp-system/
├── build.gradle.kts
├── settings.gradle.kts
├── gradle.properties
├── docker-compose.yml
├── Dockerfile
├── README.md
├── .gitignore
├── .editorconfig
├── .env
│
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
│
├── docs/
│   ├── architecture/
│   │   ├── context-diagram.md
│   │   ├── containers-diagram.md
│   │   ├── components-diagram.md
│   │   ├── domain-overview.md
│   │   ├── bounded-contexts.md
│   │   └── decisions/
│   │       ├── adr-001-modular-monolith.md
│   │       ├── adr-002-clean-architecture.md
│   │       └── adr-003-ddd.md
│   │
│   ├── api/
│   │   ├── openapi.yaml
│   │   └── postman-collection.json
│   │
│   └── database/
│       ├── schema.drawio
│       └── migrations.md
│
├── scripts/
│   ├── start-local.sh
│   ├── build.sh
│   ├── deploy.sh
│   └── test.sh
│
├── src/
│   ├── main/
│   │   ├── kotlin/
│   │   │   └── com/
│   │   │       └── company/
│   │   │           └── erp/
│   │   │               │
│   │   │               ├── ErpApplication.kt
│   │   │               │
│   │   │               ├── shared/
│   │   │               │   │
│   │   │               │   ├── kernel/
│   │   │               │   │   ├── domain/
│   │   │               │   │   │   ├── AggregateRoot.kt
│   │   │               │   │   │   ├── Entity.kt
│   │   │               │   │   │   ├── ValueObject.kt
│   │   │               │   │   │   ├── DomainEvent.kt
│   │   │               │   │   │   ├── Identifier.kt
│   │   │               │   │   │   └── AuditableEntity.kt
│   │   │               │   │   │
│   │   │               │   │   ├── application/
│   │   │               │   │   │   ├── UseCase.kt
│   │   │               │   │   │   ├── Command.kt
│   │   │               │   │   │   ├── Query.kt
│   │   │               │   │   │   ├── CommandHandler.kt
│   │   │               │   │   │   ├── QueryHandler.kt
│   │   │               │   │   │   └── EventPublisher.kt
│   │   │               │   │   │
│   │   │               │   │   ├── infrastructure/
│   │   │               │   │   │   ├── BaseEntity.kt
│   │   │               │   │   │   ├── BaseRepository.kt
│   │   │               │   │   │   ├── JpaConfig.kt
│   │   │               │   │   │   └── TransactionConfig.kt
│   │   │               │   │   │
│   │   │               │   │   └── exception/
│   │   │               │   │       ├── BusinessException.kt
│   │   │               │   │       ├── ValidationException.kt
│   │   │               │   │       ├── NotFoundException.kt
│   │   │               │   │       └── DomainException.kt
│   │   │               │   │
│   │   │               │   ├── config/
│   │   │               │   │   ├── JacksonConfig.kt
│   │   │               │   │   ├── SecurityConfig.kt
│   │   │               │   │   ├── OpenApiConfig.kt
│   │   │               │   │   ├── CacheConfig.kt
│   │   │               │   │   ├── KafkaConfig.kt
│   │   │               │   │   └── ClockConfig.kt
│   │   │               │   │
│   │   │               │   ├── security/
│   │   │               │   │   ├── JwtService.kt
│   │   │               │   │   ├── JwtAuthenticationFilter.kt
│   │   │               │   │   ├── UserPrincipal.kt
│   │   │               │   │   └── AuthenticatedUser.kt
│   │   │               │   │
│   │   │               │   ├── observability/
│   │   │               │   │   ├── LoggingInterceptor.kt
│   │   │               │   │   ├── MetricsConfig.kt
│   │   │               │   │   ├── TracingConfig.kt
│   │   │               │   │   └── CorrelationIdFilter.kt
│   │   │               │   │
│   │   │               │   ├── messaging/
│   │   │               │   │   ├── InternalEventBus.kt
│   │   │               │   │   ├── KafkaEventPublisher.kt
│   │   │               │   │   └── EventSerializer.kt
│   │   │               │   │
│   │   │               │   └── web/
│   │   │               │       ├── ApiErrorResponse.kt
│   │   │               │       ├── GlobalExceptionHandler.kt
│   │   │               │       ├── PaginationRequest.kt
│   │   │               │       └── PaginationResponse.kt
│   │   │               │
│   │   │               ├── modules/
│   │   │               │
│   │   │               │   ├── customer/
│   │   │               │   │   │
│   │   │               │   │   ├── domain/
│   │   │               │   │   │   ├── model/
│   │   │               │   │   │   │   ├── Customer.kt
│   │   │               │   │   │   │   ├── CustomerAddress.kt
│   │   │               │   │   │   │   ├── CustomerDocument.kt
│   │   │               │   │   │   │   ├── CustomerStatus.kt
│   │   │               │   │   │   │   └── CustomerId.kt
│   │   │               │   │   │   │
│   │   │               │   │   │   ├── event/
│   │   │               │   │   │   │   ├── CustomerCreatedEvent.kt
│   │   │               │   │   │   │   ├── CustomerUpdatedEvent.kt
│   │   │               │   │   │   │   └── CustomerDeletedEvent.kt
│   │   │               │   │   │   │
│   │   │               │   │   │   ├── service/
│   │   │               │   │   │   │   ├── CustomerDomainService.kt
│   │   │               │   │   │   │   └── CustomerValidationService.kt
│   │   │               │   │   │   │
│   │   │               │   │   │   └── repository/
│   │   │               │   │   │       └── CustomerRepository.kt
│   │   │               │   │   │
│   │   │               │   │   ├── application/
│   │   │               │   │   │   ├── usecase/
│   │   │               │   │   │   │   ├── createcustomer/
│   │   │               │   │   │   │   │   ├── CreateCustomerCommand.kt
│   │   │               │   │   │   │   │   ├── CreateCustomerUseCase.kt
│   │   │               │   │   │   │   │   ├── CreateCustomerResponse.kt
│   │   │               │   │   │   │   │   └── CreateCustomerValidator.kt
│   │   │               │   │   │   │   │
│   │   │               │   │   │   │   ├── updatecustomer/
│   │   │               │   │   │   │   │   ├── UpdateCustomerCommand.kt
│   │   │               │   │   │   │   │   ├── UpdateCustomerUseCase.kt
│   │   │               │   │   │   │   │   └── UpdateCustomerValidator.kt
│   │   │               │   │   │   │   │
│   │   │               │   │   │   │   ├── getcustomer/
│   │   │               │   │   │   │   │   ├── GetCustomerQuery.kt
│   │   │               │   │   │   │   │   ├── GetCustomerUseCase.kt
│   │   │               │   │   │   │   │   └── CustomerDetailsResponse.kt
│   │   │               │   │   │   │   │
│   │   │               │   │   │   │   ├── listcustomers/
│   │   │               │   │   │   │   │   ├── ListCustomersQuery.kt
│   │   │               │   │   │   │   │   ├── ListCustomersUseCase.kt
│   │   │               │   │   │   │   │   └── CustomerSummaryResponse.kt
│   │   │               │   │   │   │   │
│   │   │               │   │   │   │   └── deletecustomer/
│   │   │               │   │   │   │       ├── DeleteCustomerCommand.kt
│   │   │               │   │   │   │       └── DeleteCustomerUseCase.kt
│   │   │               │   │   │   │
│   │   │               │   │   │   ├── port/
│   │   │               │   │   │   │   ├── input/
│   │   │               │   │   │   │   │   ├── CreateCustomerInputPort.kt
│   │   │               │   │   │   │   │   ├── UpdateCustomerInputPort.kt
│   │   │               │   │   │   │   │   └── GetCustomerInputPort.kt
│   │   │               │   │   │   │   │
│   │   │               │   │   │   │   └── output/
│   │   │               │   │   │   │       ├── LoadCustomerPort.kt
│   │   │               │   │   │   │       ├── SaveCustomerPort.kt
│   │   │               │   │   │   │       └── DeleteCustomerPort.kt
│   │   │               │   │   │   │
│   │   │               │   │   │   ├── mapper/
│   │   │               │   │   │   │   ├── CustomerMapper.kt
│   │   │               │   │   │   │   └── CustomerResponseMapper.kt
│   │   │               │   │   │   │
│   │   │               │   │   │   └── eventhandler/
│   │   │               │   │   │       └── CustomerCreatedEventHandler.kt
│   │   │               │   │   │
│   │   │               │   │   ├── infrastructure/
│   │   │               │   │   │   ├── persistence/
│   │   │               │   │   │   │   ├── entity/
│   │   │               │   │   │   │   │   ├── CustomerJpaEntity.kt
│   │   │               │   │   │   │   │   └── CustomerAddressEmbeddable.kt
│   │   │               │   │   │   │   │
│   │   │               │   │   │   │   ├── repository/
│   │   │               │   │   │   │   │   ├── JpaCustomerRepository.kt
│   │   │               │   │   │   │   │   └── SpringDataCustomerRepository.kt
│   │   │               │   │   │   │   │
│   │   │               │   │   │   │   └── mapper/
│   │   │               │   │   │   │       ├── CustomerJpaMapper.kt
│   │   │               │   │   │   │       └── CustomerEntityMapper.kt
│   │   │               │   │   │   │
│   │   │               │   │   │   ├── web/
│   │   │               │   │   │   │   ├── CustomerController.kt
│   │   │               │   │   │   │   ├── request/
│   │   │               │   │   │   │   │   ├── CreateCustomerRequest.kt
│   │   │               │   │   │   │   │   └── UpdateCustomerRequest.kt
│   │   │               │   │   │   │   │
│   │   │               │   │   │   │   └── response/
│   │   │               │   │   │   │       ├── CustomerResponse.kt
│   │   │               │   │   │   │       └── CustomerListResponse.kt
│   │   │               │   │   │   │
│   │   │               │   │   │   ├── messaging/
│   │   │               │   │   │   │   ├── consumer/
│   │   │               │   │   │   │   │   └── CustomerEventConsumer.kt
│   │   │               │   │   │   │   │
│   │   │               │   │   │   │   └── producer/
│   │   │               │   │   │   │       └── CustomerEventProducer.kt
│   │   │               │   │   │   │
│   │   │               │   │   │   └── config/
│   │   │               │   │   │       └── CustomerModuleConfig.kt
│   │   │               │   │   │
│   │   │               │   │   └── CustomerModule.kt
│   │   │               │   │
│   │   │               │   ├── order/
│   │   │               │   │   ├── domain/
│   │   │               │   │   ├── application/
│   │   │               │   │   ├── infrastructure/
│   │   │               │   │   └── OrderModule.kt
│   │   │               │   │
│   │   │               │   ├── payment/
│   │   │               │   │   ├── domain/
│   │   │               │   │   ├── application/
│   │   │               │   │   ├── infrastructure/
│   │   │               │   │   └── PaymentModule.kt
│   │   │               │   │
│   │   │               │   ├── inventory/
│   │   │               │   │   ├── domain/
│   │   │               │   │   ├── application/
│   │   │               │   │   ├── infrastructure/
│   │   │               │   │   └── InventoryModule.kt
│   │   │               │   │
│   │   │               │   └── auth/
│   │   │               │       ├── domain/
│   │   │               │       ├── application/
│   │   │               │       ├── infrastructure/
│   │   │               │       └── AuthModule.kt
│   │   │               │
│   │   │               └── bootstrap/
│   │   │                   ├── DatabaseSeeder.kt
│   │   │                   ├── ModuleRegistrar.kt
│   │   │                   └── StartupRunner.kt
│   │   │
│   │   └── resources/
│   │       ├── application.yml
│   │       ├── application-dev.yml
│   │       ├── application-test.yml
│   │       ├── application-prod.yml
│   │       │
│   │       ├── db/
│   │       │   └── migration/
│   │       │       ├── V1__create_customer_table.sql
│   │       │       ├── V2__create_order_table.sql
│   │       │       ├── V3__create_payment_table.sql
│   │       │       └── V4__create_inventory_table.sql
│   │       │
│   │       ├── banner.txt
│   │       ├── logback-spring.xml
│   │       └── messages/
│   │           ├── messages.properties
│   │           └── validation.properties
│   │
│   └── test/
│       └── kotlin/
│           └── com/
│               └── company/
│                   └── erp/
│                       ├── integration/
│                       │   ├── CustomerIntegrationTest.kt
│                       │   ├── OrderIntegrationTest.kt
│                       │   └── PaymentIntegrationTest.kt
│                       │
│                       ├── unit/
│                       │   ├── customer/
│                       │   │   ├── domain/
│                       │   │   ├── application/
│                       │   │   └── infrastructure/
│                       │   │
│                       │   ├── order/
│                       │   └── payment/
│                       │
│                       ├── architecture/
│                       │   ├── ModularizationTest.kt
│                       │   ├── LayerDependencyTest.kt
│                       │   ├── HexagonalRulesTest.kt
│                       │   └── DddConventionsTest.kt
│                       │
│                       └── e2e/
│                           ├── CustomerFlowE2ETest.kt
│                           └── OrderFlowE2ETest.kt
│
└── .github/
    └── workflows/
        ├── ci.yml
        ├── cd.yml
        └── code-quality.yml

Funcionamento:

Baseado na arquitetura Layer-based Structure com módulos.


resources
 ├── application.yml # Configurações gerais da aplicação, como porta, datasource, logging, etc.
 ├── application-dev.yml # Configurações específicas para ambiente de desenvolvimento, como banco de dados local, logging detalhado, etc.
 ├── application-prod.yml # Configurações específicas para ambiente de produção, como banco de dados em nuvem, logging otimizado, etc.
 ├── static # Diretório com arquivos estáticos, como CSS, JavaScript e imagens, usados para servir conteúdo estático (se aplicável)
 ├── templates # Diretório com arquivos de template, como HTML ou Thymeleaf, usados para renderizar páginas (se aplicável)
 └── db # Scripts de banco de dados, como migrações SQL ou arquivos de configuração do Flyway/Liquibase
     └── migration # Scripts de migração do banco de dados, nomeados seguindo convenção (V1__create_users_table.sql, V2__add_products_table.sql, etc.)
         ├── V1__create_users_table.sql # Exemplo de script de migração para criar tabela de usuários
         └── V2__add_products_table.sql # Exemplo de script de migração para adicionar tabela de produtos

com.whateverer.config # Configurações globais
 ├── SwaggerConfig.kt # Configuração do Swagger para documentação da API
 ├── SecurityConfig.kt # Configuração de segurança, incluindo autenticação, autorização, filtros de segurança e rotas públicas/privadas
 ├── JwtConfig.kt # Configuração específica para JWT (JSON Web Token), como geração e validação de tokens, além de expiração e assinatura
 ├── OpenApiConfig.kt # Configuração da documentação OpenAPI, contendo título da API, versão, descrição e autenticação JWT no Swagger
 ├── JacksonConfig.kt # Configuração do serializer/deserializer JSON do Jackson, que converte Kotlin objects para JSON e vice-versa, como formatos de datas, null handling
 ├── CorsConfig.kt # Configuração de CORS (Cross-Origin Resource Sharing), definindo quais domínios podem acessar a API (como frontend), métodos HTTP permitidos e cabeçalhos
 ├── DatabaseConfig.kt # Configuração do banco de dados, incluindo conexão, datasource, JPA/Hibernate, pool de conexões, transações e demais propriedades de acesso ao banco
 ├── CacheConfig.kt # Configuração de cache, definindo quais dados devem ser armazenados em cache, tempo de expiração e estratégia de cache (como Redis, cache local ou Caffeine) para otimização de desempenho e redução de queries
 └── WebMvcConfig.kt # Configuração do Spring MVC (Model-View-Controller), incluindo resolvers, interceptors, formatadores, conversores, manipuladores de exceção e outras customizações relacionadas ao comportamento da camada web geral

com.whateverer.feature.account # Funcionalidades relacionadas ao domínio de gestão de contas e usuários
 ├── controller # Camada de controle, responsável por receber requisições HTTP REST, validar dados de entrada, chamar os serviços e retornar respostas adequadas
 │     ├── AccountController.kt # Controlador para endpoints principais de conta (criar, obter, atualizar, excluir)
 │     ├── AccountAdminController.kt # Controlador para endpoints administrativos de conta (listar contas, alterar status, atribuir papéis, bloquear contas)
 │     └── AccountAuthController.kt # Controlador para endpoints de autenticação e autorização (login, logout, refresh token)
 │
 ├── model # Camada de modelo, contendo definições de dados usados na aplicação, como DTOs (Data Transfer Objects) para comunicação entre camadas e entidades JPA para persistência no banco de dados
 │     ├── dto # Pacote de DTOs, objetos usados para transferir dados entre camadas em comunicação HTTP, como requisições e respostas da API
 │     │     ├── CreateAccountRequest.kt # Dados recebidos ao criar conta
 │     │     ├── UpdateAccountRequest.kt # Dados recebidos ao atualizar conta
 │     │     ├── AccountResponse.kt # Dados retornados/resposta ao obter detalhes de conta
 │     │     ├── AccountSummaryResponse.kt # Dados retornados/resumo ao listar contas
 │     │     └── AccountLoginRequest.kt # Payload, dados recebidos ao realizar login
 │     │
 │     └── entity # Pacote de entidades, classes que representam tabelas do banco de dados usando JPA/Hibernate, contendo anotações de mapeamento e relacionamentos
 │           ├── AccountEntity.kt # Entidade principal de conta, representando tabela principal de contas no banco de dados
 │           ├── AccountProfileEntity.kt # Entidade de perfil de conta, representando informações adicionais do usuário, como nome, email, telefone
 │           ├── AccountRoleEntity.kt # Entidade de papel de conta, representando os papéis/roles/permissões atribuídos a cada conta para controle de acesso (admin, user)
 │           └── AccountStatus.kt # Enumeração de status de conta, representando possíveis estados de conta (ativa, inativa, bloqueada, etc.)
 │
 ├── repository # Camada de repositório, responsável por acessar banco de dados e realizar operações de CRUD (Create, Read, Update, Delete) via JPA/Hibernate, além de consultas personalizadas
 │     ├── AccountRepository.kt # Repositório principal de conta, estendendo JpaRepository para operações básicas de CRUD
 │     ├── AccountQueryRepository.kt # Repositório de consultas personalizadas, contendo métodos para consultas/queries complexas ou específicas que não se encaixam no padrão do JpaRepository
 │     └── AccountCustomRepository.kt # Repositório customizado, contendo implementações personalizadas de métodos de acesso a dados que exigem lógica específica/personalizada ou otimizações
 │
 ├── service # Camada de serviço, responsável por implementar lógica de negócio da aplicação, orquestrando operações entre repositórios, validando regras de negócio e garantindo a integridade dos dados
 │     ├── AccountService.kt # Serviço principal de conta, contendo regras/métodos para criar, atualizar, excluir e obter contas, além de aplicar regras de negócio relacionadas a contas
 │     ├── CreateAccountService.kt # Serviço específico para criação de contas, contendo regras/lógica de negócio relacionada à criação, como validação de dados, verificação de duplicidade, atribuição de papéis padrão, etc
 │     ├── UpdateAccountService.kt # Serviço específico para atualização de contas, contendo regras/lógica de negócio relacionada à atualização, como validação de dados, controle de alterações, etc
 │     ├── DeleteAccountService.kt # Serviço específico para exclusão de contas, contendo regras/lógica de negócio relacionada à exclusão, como verificação de dependências, controle de status, etc
 │     ├── GetAccountService.kt # Serviço específico para obtenção de detalhes de conta individual, contendo regras/lógica de negócio relacionada à obtenção, como controle de acesso, formatação de dados, etc
 │     ├── ListAccountService.kt # Serviço específico para listagem de contas, contendo regras/lógica de negócio relacionada à listagem, como paginação, filtragem, ordenação, etc
 │     ├── AccountValidationService.kt # Serviço de validação de conta, contendo regras/lógica de negócio relacionada à validação de dados de conta, como validação de email, senha, formato de dados, etc
 │     └── AccountAuthService.kt # Serviço de autenticação e autorização de conta, contendo regras/lógica de negócio relacionada à autenticação (login, logout, refresh token) e controle de acesso (verificação de papéis/permissões)
 │
 └── utils # Camada de utilitários, contendo classes e métodos auxiliares para mapeamento de objetos (DTOs para entidades e vice-versa), validação de dados, constantes e outras funções utilitárias relacionadas ao domínio de conta
       ├── AccountMapper.kt # Mapeador para conversão entre entidades em DTOs, e vice-versa, de conta
       ├── AccountUtils.kt # Utilitários/funções diversas relacionados a conta, como formatação de dados, geração de senhas, etc
       ├── AccountValidator.kt # Validador para validações reutilizáveis de dados relacionados a conta, como validação de email, senha, formato de dados, etc
       ├── AccountConstants.kt # Constantes relacionadas a conta, como mensagens de erro, papéis padrão, etc
       └── AccountPermissions.kt # Definição/centralização de permissões e roles/papéis relacionados a conta, para controle de acesso

com.whateverer.feature.payment
 ├── controller
 │     ├── PaymentController.kt
 │     ├── RefundController.kt
 │     └── PaymentWebhookController.kt
 │
 ├── model
 │     ├── dto
 │     │     ├── CreatePaymentRequest.kt
 │     │     ├── PaymentResponse.kt
 │     │     ├── RefundRequest.kt
 │     │     ├── PaymentStatusResponse.kt
 │     │     └── InstallmentSimulationResponse.kt
 │     │
 │     └── entity
 │           ├── PaymentEntity.kt
 │           ├── RefundEntity.kt
 │           ├── PaymentMethodEntity.kt
 │           └── PaymentStatus.kt
 │
 ├── repository
 │     ├── PaymentRepository.kt
 │     ├── RefundRepository.kt
 │     └── PaymentQueryRepository.kt
 │
 ├── service
 │     ├── PaymentService.kt
 │     ├── RefundService.kt
 │     ├── PaymentGatewayService.kt
 │     ├── PaymentValidationService.kt
 │     ├── InstallmentService.kt
 │     └── PaymentWebhookService.kt
 │
 └── utils
       ├── PaymentMapper.kt
       ├── PaymentUtils.kt
       ├── PaymentValidator.kt
       ├── PaymentConstants.kt
       └── InstallmentCalculator.kt

src.test.kotlin.com.whateverer # Pacote de testes, organizado por tipo de teste (integração, unitário, arquitetura, e2e) e por domínio/feature (account, payment, order)
├── integration # Testes de integração, que verificam interação entre diferentes módulos ou camadas do sistema
│   ├── AccountIntegrationTest.kt # Teste de integração para funcionalidades relacionadas a conta, verificando interação entre camadas (controller, service, repository) e comportamento geral da funcionalidade de conta
│   ├── PaymentIntegrationTest.kt # Teste de integração para funcionalidades relacionadas a pagamento, verificando interação entre camadas (controller, service, repository) e comportamento geral da funcionalidade de pagamento
│   └── OrderIntegrationTest.kt # Teste de integração para funcionalidades relacionadas a pedido, verificando interação entre camadas (controller, service, repository) e comportamento geral da funcionalidade de pedido
├── unit # Testes unitários, que verificam comportamento de unidades isoladas de código, como métodos ou classes específicas, usando mocks para dependências externas
│   ├── account # Testes unitários para funcionalidades relacionadas a conta, organizados por camada (controller, service, repository)
│   │   ├── controller # Testes unitários para camada de controle de conta, verificando comportamento dos endpoints REST, validação de dados de entrada e respostas adequadas
│   │   │   └── AccountControllerTest.kt # Teste unitário para AccountController, verificando comportamento dos endpoints de conta
│   │   ├── service # Testes unitários para camada de serviço de conta, verificando lógica de negócio, regras de validação e interação com repositórios
│   │   │   ├── AccountServiceTest.kt # Teste unitário para AccountService, verificando regras de negócio relacionadas a conta
│   │   │   └── AccountValidationServiceTest.kt # Teste unitário para AccountValidationService, verificando regras de validação de dados relacionados a conta
│   │   └── repository # Testes unitários para camada de repositório de conta, verificando operações de acesso a dados e consultas personalizadas
│   │       ├── AccountRepositoryTest.kt # Teste unitário para AccountRepository, verificando operações básicas de CRUD e consultas simples
│   │       └── AccountQueryRepositoryTest.kt # Teste unitário para AccountQueryRepository, verificando consultas personalizadas e complexas relacionadas a conta
│   ├── payment
│   └── order
├── architecture # Testes de arquitetura, que verificam conformidade do código com regras de arquitetura definidas, como modularização, dependências entre camadas, regras hexagonais e convenções de DDD
│   ├── ModularizationTest.kt # Teste de arquitetura para verificar modularização do código, garantindo que funcionalidades estejam organizadas em módulos coesos e com baixo acoplamento
│   ├── LayerDependencyTest.kt # Teste de arquitetura para verificar dependências entre camadas, garantindo que camadas superiores (controller) dependam apenas das camadas inferiores (service, repository) e não o contrário
│   ├── HexagonalRulesTest.kt # Teste de arquitetura para verificar regras hexagonais, garantindo que camada de domínio não dependa de detalhes de infraestrutura e que portas e adaptadores estejam corretamente implementados
│   └── DddConventionsTest.kt # Teste de arquitetura para verificar convenções de DDD (Domain-Driven Design), garantindo que entidades, agregados, repositórios e serviços estejam organizados de acordo com práticas recomendadas de DDD
└── e2e # Testes end-to-end, que verificam comportamento do sistema como todo, simulando cenários reais de uso e verificando integração entre todas camadas e módulos
    ├── CustomerFlowE2ETest.kt # Teste end-to-end para fluxo de cliente, simulando cenários reais de uso relacionados a clientes, como criação, atualização, obtenção e exclusão de clientes, verificando integração entre todas as camadas e módulos relacionados a cliente
    └── OrderFlowE2ETest.kt # Teste end-to-end para fluxo de pedido, simulando cenários reais de uso relacionados a pedidos, como criação, atualização, obtenção e exclusão de pedidos, verificando integração entre todas camadas e módulos relacionados a pedido

Fluxo para criação de nova feature no projeto:

  1. Criar Entity (AccountEntity), definindo tables, columns, constraints e relacionamentos;
  2. Criar DTOs (CreateAccountRequest, AccountResponse), definindo entrada e saída de dados da API;
  3. Criar Repository (AccountRepository), definindo operações de acesso ao banco de dados;
  4. Criar Mapper (AccountMapper), definindo mapeamento/conversão entre entidades e DTOs;
  5. Criar Validators (AccountValidator), definindo regras de validação de dados, campos e regras de negócio para criação e atualização de contas;
  6. Criar Services (AccountService), definindo regras e lógica de negócio para manipulação de contas, no fluxo do CRUD (create, update, delete, get, list);
  7. Criar Controllers (AccountController), definindo endpoints da API REST para exposição das operações de CRUD (POST /accounts, GET /accounts, PUT /accounts/{id}, DELETE /accounts/{id});
  8. Configurar segurança (JWT, OAuth2, etc), definindo autenticação, autorização, roles e gerenciamento de tokens;
  9. Documentar API (Swagger, OpenAPI), definindo especificações e documentação para endpoints da API;
  10. Criar testes (Unitários, Integração, End-to-End, Controller, Repository), definindo casos de teste para verificar o comportamento do sistema e garantir a qualidade do código.

Fluxo de Request:

  1. Cliente HTTP (envia requisição);
  2. Controller (entrada, recebe endpoint);
  3. Service (executa regras de negócio);
  4. Validation (valida dados);
  5. Repository (acessa banco de dados);
  6. Banco de Dados (persiste dados);
  7. Mapper (converte objetos Entidade e DTO);
  8. Response DTO (monta resposta de retorno);
  9. Cliente (recebe resultado, resposta).


API REST Spring Boot Kotlin (sem JWT):

Modelo de API REST Spring Boot e Kotlin, sem utilização de JWT para autenticação, nem criação de testes. Pré-requisitos são Java JDK 25, Kotlin compiler 2.4, Spring Boot 4.1, Gradle 9.6, VS Code (extensões Java, Spring Boot, Kotlin JetBrains, Postman, DBCode), conta no Postman, e Docker. Hierarquia de diretórios Feature-Based Architecture (Package by Feature) básica:


cat_api_simples
├── build.gradle.kts
├── settings.gradle.kts
└── src
    └── main
        ├── kotlin
        │   └── com
        │       └── ubsocial
        │           └── cat_api_simples
        │               ├── Application.kt
        │               ├── config
        │               │   └── CorsConfig.kt
        │               ├── exception
        │               │   ├── BusinessException.kt
        │               │   ├── ResourceNotFoundException.kt
        │               │   ├── ErrorResponse.kt
        │               │   └── GlobalExceptionHandler.kt
        │               └── feature
        │                   └── cat
        │                       ├── controller
        │                       │   ├── CatController.kt
        │                       │   └── CatAdoptionController.kt
        │                       ├── mapper
        │                       │   └── CatMapper.kt
        │                       ├── model
        │                       │   ├── dto
        │                       │   │   ├── CreateCatRequest.kt
        │                       │   │   ├── UpdateCatRequest.kt
        │                       │   │   ├── AdoptCatRequest.kt
        │                       │   │   ├── RemoveAdoptionRequest.kt
        │                       │   │   ├── CatResponse.kt
        │                       │   │   └── CatSummaryResponse.kt
        │                       │   ├── entity
        │                       │   │   └── CatEntity.kt
        │                       │   └── enums
        │                       │       └── CatStatus.kt
        │                       ├── repository
        │                       │   └── CatRepository.kt
        │                       ├── service
        │                       │   ├── CatService.kt
        │                       │   ├── CreateCatService.kt
        │                       │   ├── UpdateCatService.kt
        │                       │   ├── DeleteCatService.kt
        │                       │   ├── GetCatService.kt
        │                       │   ├── ListCatService.kt
        │                       │   ├── AdoptCatService.kt
        │                       │   └── RemoveAdoptionService.kt
        │                       ├── validator
        │                       │   └── CatValidator.kt
        │                       └── utils
        │                           └── CatUtils.kt
        └── resources
            └── application.properties
  1. Gerar projeto em start.spring.io:
    • Project: Gradle - Kotlin
    • Language: kotlin
    • Spring Boot: 4.1.0
    • Project Metadata:
    • Group: com.ubsocial
    • Artifact: cat_api_simples
    • Package name: com.ubsocial.cat_api_simples
    • Packaging: Jar
    • Configuration: Properties
    • Java: 25
    • Dependencies: Spring Web, Spring Data JPA, Validation, Oracle Driver
    • Gerar projeto e extrair
  2. Criar container Docker com Oracle Database (comandos terminal):
    
    docker pull container-registry.oracle.com/database/free:latest
    docker run -d --name oracle-db -p 1521:1521 -p 5500:5500 -e ORACLE_PWD=SenhaForte123 container-registry.oracle.com/database/free:latest
    
    • Driver: Oracle
    • Connection Type: Service Name
    • Hostname: localhost
    • Port: 1521
    • Service: FREEPDB1
    • Driver: Thin
    • Authentication: User & Password
    • User: system
    • Password: SenhaForte123
    • Testar conexão e confirmar
  3. No editor SQL conectado ao container Oracle DB, criar usuário e senha para o projeto (executar 1 linha por vez):
    
    CREATE USER catapi IDENTIFIED BY "CatApi@123";
    GRANT CONNECT TO catapi;
    GRANT RESOURCE TO catapi;
    GRANT CREATE SESSION TO catapi;
    GRANT CREATE TABLE TO catapi;
    GRANT CREATE VIEW TO catapi;
    GRANT CREATE SEQUENCE TO catapi;
    GRANT CREATE TRIGGER TO catapi;
    GRANT CREATE PROCEDURE TO catapi;
    GRANT UNLIMITED TABLESPACE TO catapi;
    
  4. Editar conexão do Oracle DB para novo usuário:
    • Usuário: catapi
    • Senha: CatApi@123
    • Testar conexão e confirmar
  5. Executar comando SQL 'SELECT USER FROM DUAL' e verificar se usuário 'CATAPI' existe
  6. Codificar src/main/resources/application.properties, com informações de conexão com banco de dados Oracle:
    
    spring.application.name=cat_api_simples
    
    spring.datasource.url=jdbc:oracle:thin:@//localhost:1521/FREEPDB1
    spring.datasource.username=catapi
    spring.datasource.password=CatApi@123
    spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
    
    spring.jpa.hibernate.ddl-auto=update
    spring.jpa.open-in-view=false
    spring.jpa.show-sql=true
    
    spring.jpa.properties.hibernate.format_sql=true
    spring.jpa.properties.hibernate.jdbc.time_zone=UTC
    
    spring.web.error.include-message=always
    spring.web.error.include-binding-errors=always
    spring.web.error.include-stacktrace=never
    spring.web.error.include-exception=false
    
  7. No pacote principal 'cat_api_simples', criar pacote 'exception', com classes de exceção personalizadas
    1. 'BusinessException.kt', contendo classe BusinessException, estendendo RuntimeException:
      
      package com.ubsocial.cat_api_simples.exception
      
      class BusinessException(
          message: String
      ) : RuntimeException(message)
      
    2. 'ResourceNotFoundException.kt', contendo classe ResourceNotFoundException, estendendo RuntimeException:
      
      package com.ubsocial.cat_api_simples.exception
      
      class ResourceNotFoundException(
          message: String
      ) : RuntimeException(message)
      
    3. 'ErrorResponse.kt', contendo classe ErrorResponse, com atributos timestamp, status, error, message e path:
      
      package com.ubsocial.cat_api_simples.exception
      
      import java.time.LocalDateTime
      
      data class ErrorResponse(
          val timestamp: LocalDateTime,
          val status: Int,
          val error: String,
          val message: String,
          val path: String
      )
      
    4. 'GlobalExceptionHandler.kt', contendo classe GlobalExceptionHandler, com métodos para tratar exceções globais:
      
      package com.ubsocial.cat_api_simples.exception
      
      import jakarta.servlet.http.HttpServletRequest
      import org.springframework.http.HttpStatus
      import org.springframework.http.ResponseEntity
      import org.springframework.web.bind.MethodArgumentNotValidException
      import org.springframework.web.bind.annotation.ExceptionHandler
      import org.springframework.web.bind.annotation.RestControllerAdvice
      import java.time.LocalDateTime
      
      @RestControllerAdvice
      class GlobalExceptionHandler {
      
          @ExceptionHandler(ResourceNotFoundException::class)
          fun handleResourceNotFoundException(
              exception: ResourceNotFoundException,
              request: HttpServletRequest
          ): ResponseEntity<ErrorResponse> {
      
              val errorResponse = ErrorResponse(
                  timestamp = LocalDateTime.now(),
                  status = HttpStatus.NOT_FOUND.value(),
                  error = HttpStatus.NOT_FOUND.reasonPhrase,
                  message = exception.message ?: "Resource not found.",
                  path = request.requestURI
              )
      
              return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse)
          }
      
          @ExceptionHandler(BusinessException::class)
          fun handleBusinessException(
              exception: BusinessException,
              request: HttpServletRequest
          ): ResponseEntity<ErrorResponse> {
      
              val errorResponse = ErrorResponse(
                  timestamp = LocalDateTime.now(),
                  status = HttpStatus.BAD_REQUEST.value(),
                  error = HttpStatus.BAD_REQUEST.reasonPhrase,
                  message = exception.message ?: "Business rule violation.",
                  path = request.requestURI
              )
      
              return ResponseEntity.badRequest().body(errorResponse)
          }
      
          @ExceptionHandler(MethodArgumentNotValidException::class)
          fun handleValidationException(
              exception: MethodArgumentNotValidException,
              request: HttpServletRequest
          ): ResponseEntity<ErrorResponse> {
      
              val message = exception.bindingResult
                  .fieldErrors
                  .joinToString(", ") { "${it.field}: ${it.defaultMessage}" }
      
              val errorResponse = ErrorResponse(
                  timestamp = LocalDateTime.now(),
                  status = HttpStatus.BAD_REQUEST.value(),
                  error = HttpStatus.BAD_REQUEST.reasonPhrase,
                  message = message,
                  path = request.requestURI
              )
      
              return ResponseEntity.badRequest().body(errorResponse)
          }
      
          @ExceptionHandler(Exception::class)
          fun handleException(
              exception: Exception,
              request: HttpServletRequest
          ): ResponseEntity<ErrorResponse> {
      
              val errorResponse = ErrorResponse(
                  timestamp = LocalDateTime.now(),
                  status = HttpStatus.INTERNAL_SERVER_ERROR.value(),
                  error = HttpStatus.INTERNAL_SERVER_ERROR.reasonPhrase,
                  message = "An unexpected error has occurred.",
                  path = request.requestURI
              )
      
              return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                  .body(errorResponse)
          }
      }
      
  8. No pacote principal 'com.ubsocial.cat_api_simples', criar pacote 'config', com classes de configuração da aplicação:
    1. 'config/CorsConfig.kt', contendo classe CorsConfig, com configuração de CORS para permitir requisições de qualquer origem externa:
      
      package com.ubsocial.cat_api_simples.config
      
      import org.springframework.context.annotation.Bean
      import org.springframework.context.annotation.Configuration
      import org.springframework.web.cors.CorsConfiguration
      import org.springframework.web.cors.UrlBasedCorsConfigurationSource
      import org.springframework.web.filter.CorsFilter
      
      @Configuration
      class CorsConfig {
      
          @Bean
          fun corsFilter(): CorsFilter {
              val configuration = CorsConfiguration().apply {
                  allowedOriginPatterns = listOf("*")
                  allowedMethods = listOf("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
                  allowedHeaders = listOf("*")
                  allowCredentials = false
                  maxAge = 3600
              }
      
              val source = UrlBasedCorsConfigurationSource()
              source.registerCorsConfiguration("/**", configuration)
              return CorsFilter(source)
          }
      }
      
  9. No pacote principal 'com.ubsocial.cat_api_simples', criar pacote 'feature', com subpacotes para cada feature da aplicação
  10. Em 'feature', criar pacote 'cat' para funcionalidades relacionadas a gatos
  11. Em 'cat', criar pacote 'model', com classes de modelo da aplicação:
    1. Em 'model', criar pacote 'enums', com enumeração 'CatStatus.kt', contendo status possíveis para gatos (AVAILABLE, ADOPTED, PENDING)
    2. 'model/enums/CatStatus.kt', contendo enumeração CatStatus:
      
      package com.ubsocial.cat_api_simples.feature.cat.model.enums
      
      enum class CatStatus {
          AVAILABLE,
          ADOPTED
      }
      
    3. Em 'model', criar pacote 'entity', com entidade JPA 'CatEntity.kt', representando tabela de gatos no banco de dados
    4. 'model/entity/CatEntity.kt', contendo entidade CatEntity:
      
      package com.ubsocial.cat_api_simples.feature.cat.model.entity
      
      import com.ubsocial.cat_api_simples.feature.cat.model.enums.CatStatus
      import jakarta.persistence.*
      
      @Entity
      @Table(name = "cats")
      class CatEntity(
      
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          @Column(name = "id")
          var id: Long? = null,
      
          @Column(name = "name", nullable = false)
          var name: String,
      
          @Column(name = "breed", nullable = false)
          var breed: String,
      
          @Column(name = "age", nullable = false)
          var age: Int,
      
          @Enumerated(EnumType.STRING)
          @Column(name = "status", nullable = false)
          var status: CatStatus = CatStatus.AVAILABLE,
      
          @Column(name = "adopter_name")
          var adopterName: String? = null
      )
      
    5. Em 'model', criar pacote 'dto', com classes DTO para comunicação entre camadas da aplicação:
      1. 'dto/CreateCatRequest.kt', contendo classe CreateCatRequest, com atributos name, breed e age:
        
        package com.ubsocial.cat_api_simples.feature.cat.model.dto
        
        import jakarta.validation.constraints.Min
        import jakarta.validation.constraints.NotBlank
        
        data class CreateCatRequest(
        
            @field:NotBlank(message = "Nome do gato é obrigatório")
            val name: String,
        
            @field:NotBlank(message = "Raça do gato é obrigatória")
            val breed: String,
        
            @field:Min(value = 0, message = "Idade do gato deve ser maior ou igual a zero")
            val age: Int
        )
        
      2. 'dto/UpdateCatRequest.kt', contendo classe UpdateCatRequest, com atributos name, breed e age:
        
        package com.ubsocial.cat_api_simples.feature.cat.model.dto
        
        import jakarta.validation.constraints.Min
        import jakarta.validation.constraints.NotBlank
        
        data class UpdateCatRequest(
        
            @field:NotBlank(message = "Nome do gato é obrigatório")
            val name: String,
        
            @field:NotBlank(message = "Raça do gato é obrigatória")
            val breed: String,
        
            @field:Min(value = 0, message = "Idade do gato deve ser maior ou igual a zero")
            val age: Int
        )
        
      3. 'dto/AdoptCatRequest.kt', contendo classe AdoptCatRequest, com atributo adopterName:
        
        package com.ubsocial.cat_api_simples.feature.cat.model.dto
        
        import jakarta.validation.constraints.NotBlank
        
        data class AdoptCatRequest(
        
            @field:NotBlank(message = "Nome do adotante não pode ser nulo ou vazio")
            val adopterName: String
        )
        
      4. 'dto/RemoveAdoptionRequest.kt', contendo classe RemoveAdoptionRequest, com atributo reason:
        
        package com.ubsocial.cat_api_simples.feature.cat.model.dto
        
        data class RemoveAdoptionRequest(
            val reason: String? = null
        )
        
      5. 'dto/CatResponse.kt', contendo classe CatResponse, com atributos id, name, breed, age, status e adopterName:
        
        package com.ubsocial.cat_api_simples.feature.cat.model.dto
        
        import com.ubsocial.cat_api_simples.feature.cat.model.enums.CatStatus
        
        data class CatResponse(
            val id: Long,
            val name: String,
            val breed: String,
            val age: Int,
            val status: CatStatus,
            val adopterName: String?
        )
        
      6. 'dto/CatSummaryResponse.kt', contendo classe CatSummaryResponse, com atributos id, name e status:
        
        package com.ubsocial.cat_api_simples.feature.cat.model.dto
        
        import com.ubsocial.cat_api_simples.feature.cat.model.enums.CatStatus
        
        data class CatSummaryResponse(
            val id: Long,
            val name: String,
            val status: CatStatus
        )
        
  12. Em 'cat', criar pacote 'repository', com interface 'CatRepository.kt', estendendo JpaRepository para operações CRUD de gatos
  13. 'repository/CatRepository.kt', contendo interface CatRepository:
    
    package com.ubsocial.cat_api_simples.feature.cat.repository
    
    import com.ubsocial.cat_api_simples.feature.cat.model.entity.CatEntity
    import org.springframework.data.jpa.repository.JpaRepository
    import org.springframework.stereotype.Repository
    
    @Repository
    interface CatRepository : JpaRepository<CatEntity, Long>
    
  14. Em 'cat', criar pacote 'mapper', com classe 'CatMapper.kt', contendo métodos para conversão entre entidades e DTOs de gatos
  15. 'mapper/CatMapper.kt', contendo classe CatMapper:
    
    package com.ubsocial.cat_api_simples.feature.cat.mapper
    
    import com.ubsocial.cat_api_simples.feature.cat.model.dto.CatResponse
    import com.ubsocial.cat_api_simples.feature.cat.model.dto.CatSummaryResponse
    import com.ubsocial.cat_api_simples.feature.cat.model.entity.CatEntity
    import org.springframework.stereotype.Component
    
    @Component
    class CatMapper {
        fun toResponse(entity: CatEntity): CatResponse =
            CatResponse(
                id = entity.id!!,
                name = entity.name,
                breed = entity.breed,
                age = entity.age,
                status = entity.status,
                adopterName = entity.adopterName
            )
    
        fun toSummaryResponse(entity: CatEntity): CatSummaryResponse =
            CatSummaryResponse(
                id = entity.id!!,
                name = entity.name,
                status = entity.status
            )
    }
    
  16. Em 'cat', criar pacote 'validator', com classe 'CatValidator.kt', contendo métodos para validação de dados de gatos
  17. 'validator/CatValidator.kt', contendo classe CatValidator:
    
    package com.ubsocial.cat_api_simples.feature.cat.validator
    
    import com.ubsocial.cat_api_simples.exception.BusinessException
    import com.ubsocial.cat_api_simples.feature.cat.model.entity.CatEntity
    import com.ubsocial.cat_api_simples.feature.cat.model.enums.CatStatus
    import org.springframework.stereotype.Component
    
    @Component
    class CatValidator {
    
        fun validateAvailableForAdoption(cat: CatEntity) {
            if (cat.status == CatStatus.ADOPTED) {
                throw BusinessException("Gato já foi adotado")
            }
        }
    
        fun validateAdopted(cat: CatEntity) {
            if (cat.status != CatStatus.ADOPTED) {
                throw BusinessException("Gato não foi adotado")
            }
        }
    }
    
  18. Em 'cat', criar pacote 'utils', com classe 'CatUtils.kt', contendo métodos utilitários para gatos
  19. 'utils/CatUtils.kt', contendo classe CatUtils:
    
    package com.ubsocial.cat_api_simples.feature.cat.utils
    
    import org.springframework.stereotype.Component
    
    @Component
    class CatUtils {
        fun normalizeName(name: String): String =
            name.trim()
                .lowercase()
                .replaceFirstChar { it.uppercase() }
    }
    
  20. Em 'cat', criar pacote 'service', com classes de serviço para manipulação de gatos (CRUD e adoção):
    1. 'service/CatService.kt', contendo classe CatService, com métodos para criar, atualizar, deletar, obter e listar gatos:
      
      package com.ubsocial.cat_api_simples.feature.cat.service
      
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.*
       
      interface CatService {
      
          fun create(request: CreateCatRequest): CatResponse
      
          fun update(id: Long, request: UpdateCatRequest): CatResponse
      
          fun delete(id: Long)
      
          fun findById(id: Long): CatResponse
      
          fun findAll(): List<CatSummaryResponse>
      
          fun adopt(id: Long, request: AdoptCatRequest): CatResponse
      
          fun removeAdoption(id: Long, request: RemoveAdoptionRequest): CatResponse
      }
      
    2. 'service/CreateCatService.kt', contendo classe CreateCatService, implementando CatService e injetando CatRepository, CatMapper e CatValidator:
      
      package com.ubsocial.cat_api_simples.feature.cat.service
      
      import com.ubsocial.cat_api_simples.feature.cat.mapper.CatMapper
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.CatResponse
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.CreateCatRequest
      import com.ubsocial.cat_api_simples.feature.cat.model.entity.CatEntity
      import com.ubsocial.cat_api_simples.feature.cat.repository.CatRepository
      import com.ubsocial.cat_api_simples.feature.cat.utils.CatUtils
      import org.springframework.stereotype.Service
      
      @Service
      class CreateCatService(
          private val catRepository: CatRepository,
          private val catMapper: CatMapper,
          private val catUtils: CatUtils
      ) {
      
          fun execute(request: CreateCatRequest): CatResponse {
              val cat = CatEntity(
                  name = catUtils.normalizeName(request.name),
                  breed = request.breed,
                  age = request.age
              )
      
              val savedCat = catRepository.save(cat)
              return catMapper.toResponse(savedCat)
          }
      }
      
    3. 'service/UpdateCatService.kt', contendo classe UpdateCatService, implementando CatService e injetando CatRepository, CatMapper e CatValidator:
      
      package com.ubsocial.cat_api_simples.feature.cat.service
      
      import com.ubsocial.cat_api_simples.exception.ResourceNotFoundException
      import com.ubsocial.cat_api_simples.feature.cat.mapper.CatMapper
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.CatResponse
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.UpdateCatRequest
      import com.ubsocial.cat_api_simples.feature.cat.repository.CatRepository
      import com.ubsocial.cat_api_simples.feature.cat.utils.CatUtils
      import org.springframework.stereotype.Service
      
      @Service
      class UpdateCatService(
          private val catRepository: CatRepository,
          private val catMapper: CatMapper,
          private val catUtils: CatUtils
      ) {
      
          fun execute(
              id: Long,
              request: UpdateCatRequest
          ): CatResponse {
      
              val cat = catRepository.findById(id)
                  .orElseThrow {
                      ResourceNotFoundException("Gato não encontrado com id: $id")
                  }
      
              cat.name = catUtils.normalizeName(request.name)
              cat.breed = request.breed
              cat.age = request.age
      
              val updatedCat = catRepository.save(cat)
              return catMapper.toResponse(updatedCat)
          }
      }
      
    4. 'service/DeleteCatService.kt', contendo classe DeleteCatService, implementando CatService e injetando CatRepository:
      
      package com.ubsocial.cat_api_simples.feature.cat.service
      
      import com.ubsocial.cat_api_simples.exception.ResourceNotFoundException
      import com.ubsocial.cat_api_simples.feature.cat.repository.CatRepository
      import org.springframework.stereotype.Service
      
      @Service
      class DeleteCatService(
          private val catRepository: CatRepository
      ) {
      
          fun execute(id: Long) {
      
              val cat = catRepository.findById(id)
                  .orElseThrow {
                      ResourceNotFoundException("Gato não encontrado com id: $id")
                  }
      
              catRepository.delete(cat)
          }
      }
      
    5. 'service/GetCatService.kt', contendo classe GetCatService, implementando CatService e injetando CatRepository e CatMapper:
      
      package com.ubsocial.cat_api_simples.feature.cat.service
      
      import com.ubsocial.cat_api_simples.exception.ResourceNotFoundException
      import com.ubsocial.cat_api_simples.feature.cat.mapper.CatMapper
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.CatResponse
      import com.ubsocial.cat_api_simples.feature.cat.repository.CatRepository
      import org.springframework.stereotype.Service
      
      @Service
      class GetCatService(
          private val catRepository: CatRepository,
          private val catMapper: CatMapper
      ) {
      
          fun execute(id: Long): CatResponse {
      
              val cat = catRepository.findById(id)
                  .orElseThrow {
                      ResourceNotFoundException("Gato não encontrado com id: $id")
                  }
      
              return catMapper.toResponse(cat)
          }
      }
      
    6. 'service/ListCatService.kt', contendo classe ListCatService, implementando CatService e injetando CatRepository e CatMapper:
      
      package com.ubsocial.cat_api_simples.feature.cat.service
      
      import com.ubsocial.cat_api_simples.feature.cat.mapper.CatMapper
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.CatSummaryResponse
      import com.ubsocial.cat_api_simples.feature.cat.repository.CatRepository
      import org.springframework.stereotype.Service
      
      @Service
      class ListCatService(
          private val catRepository: CatRepository,
          private val catMapper: CatMapper
      ) {
      
          fun execute(): List<CatSummaryResponse> {
              return catRepository.findAll()
                  .map(catMapper::toSummaryResponse)
          }
      }
      
    7. 'service/AdoptCatService.kt', contendo classe AdoptCatService, implementando CatService e injetando CatRepository, CatMapper e CatValidator:
      
      package com.ubsocial.cat_api_simples.feature.cat.service
      
      import com.ubsocial.cat_api_simples.exception.ResourceNotFoundException
      import com.ubsocial.cat_api_simples.feature.cat.mapper.CatMapper
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.AdoptCatRequest
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.CatResponse
      import com.ubsocial.cat_api_simples.feature.cat.repository.CatRepository
      import com.ubsocial.cat_api_simples.feature.cat.validator.CatValidator
      import org.springframework.stereotype.Service
      
      @Service
      class AdoptCatService(
          private val catRepository: CatRepository,
          private val catMapper: CatMapper,
          private val catValidator: CatValidator
      ) {
      
          fun execute(
              id: Long,
              request: AdoptCatRequest
          ): CatResponse {
      
              val cat = catRepository.findById(id)
                  .orElseThrow {
                      ResourceNotFoundException("Gato não encontrado com id: $id")
                  }
      
              catValidator.validateAvailableForAdoption(cat)
      
              cat.adopterName = request.adopterName
              cat.status = com.ubsocial.cat_api_simples.feature.cat.model.enums.CatStatus.ADOPTED
      
              val adoptedCat = catRepository.save(cat)
              return catMapper.toResponse(adoptedCat)
          }
      }
      
    8. 'service/RemoveAdoptionService.kt', contendo classe RemoveAdoptionService, implementando CatService e injetando CatRepository, CatMapper e CatValidator:
      
      package com.ubsocial.cat_api_simples.feature.cat.service
      
      import com.ubsocial.cat_api_simples.exception.ResourceNotFoundException
      import com.ubsocial.cat_api_simples.feature.cat.mapper.CatMapper
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.CatResponse
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.RemoveAdoptionRequest
      import com.ubsocial.cat_api_simples.feature.cat.model.enums.CatStatus
      import com.ubsocial.cat_api_simples.feature.cat.repository.CatRepository
      import com.ubsocial.cat_api_simples.feature.cat.validator.CatValidator
      import org.springframework.stereotype.Service
      
      @Service
      class RemoveAdoptionService(
          private val catRepository: CatRepository,
          private val catMapper: CatMapper,
          private val catValidator: CatValidator
      ) {
      
          fun execute(
              id: Long,
              request: RemoveAdoptionRequest
          ): CatResponse {
      
              val cat = catRepository.findById(id)
                  .orElseThrow {
                      ResourceNotFoundException("Gato não encontrado com id: $id")
                  }
      
              catValidator.validateAdopted(cat)
      
              cat.adopterName = null
              cat.status = CatStatus.AVAILABLE
      
              val updatedCat = catRepository.save(cat)
              return catMapper.toResponse(updatedCat)
          }
      }
      
  21. Em 'cat', criar pacote 'controller', com classes de controlador REST para expor endpoints de gatos:
    1. 'controller/CatController.kt', contendo classe CatController, com endpoints para criar, atualizar, deletar, obter e listar gatos:
      
      package com.ubsocial.cat_api_simples.feature.cat.controller
      
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.CatResponse
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.CreateCatRequest
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.UpdateCatRequest
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.CatSummaryResponse
      import com.ubsocial.cat_api_simples.feature.cat.service.CreateCatService
      import com.ubsocial.cat_api_simples.feature.cat.service.DeleteCatService
      import com.ubsocial.cat_api_simples.feature.cat.service.GetCatService
      import com.ubsocial.cat_api_simples.feature.cat.service.ListCatService
      import com.ubsocial.cat_api_simples.feature.cat.service.UpdateCatService
      import jakarta.validation.Valid
      import org.springframework.http.HttpStatus
      import org.springframework.http.ResponseEntity
      import org.springframework.web.bind.annotation.*
      
      @RestController
      @RequestMapping("/cats")
      class CatController(
          private val createCatService: CreateCatService,
          private val updateCatService: UpdateCatService,
          private val deleteCatService: DeleteCatService,
          private val getCatService: GetCatService,
          private val listCatService: ListCatService
      ) {
      
          @PostMapping
          fun create(
              @Valid @RequestBody request: CreateCatRequest
          ): ResponseEntity<CatResponse> {
              return ResponseEntity
                  .status(HttpStatus.CREATED)
                  .body(createCatService.execute(request))
          }
      
          @PutMapping("/{id}")
          fun update(
              @PathVariable id: Long,
              @Valid @RequestBody request: UpdateCatRequest
          ): ResponseEntity<CatResponse> {
              return ResponseEntity.ok(
                  updateCatService.execute(id, request)
              )
          }
      
          @DeleteMapping("/{id}")
          fun delete(
              @PathVariable id: Long
          ): ResponseEntity<Void> {
              deleteCatService.execute(id)
              return ResponseEntity.noContent().build()
          }
      
          @GetMapping("/{id}")
          fun findById(
              @PathVariable id: Long
          ): ResponseEntity<CatResponse> {
              return ResponseEntity.ok(
                  getCatService.execute(id)
              )
          }
      
          @GetMapping
          fun findAll(): ResponseEntity<List<CatSummaryResponse>> {
              return ResponseEntity.ok(
                  listCatService.execute()
              )
          }
      }
      
    2. 'controller/CatAdoptionController.kt', contendo classe CatAdoptionController, com endpoints para adotar e remover adoção de gatos:
      
      package com.ubsocial.cat_api_simples.feature.cat.controller
      
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.AdoptCatRequest
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.CatResponse
      import com.ubsocial.cat_api_simples.feature.cat.model.dto.RemoveAdoptionRequest
      import com.ubsocial.cat_api_simples.feature.cat.service.AdoptCatService
      import com.ubsocial.cat_api_simples.feature.cat.service.RemoveAdoptionService
      import jakarta.validation.Valid
      import org.springframework.http.ResponseEntity
      import org.springframework.web.bind.annotation.*
      
      @RestController
      @RequestMapping("/cats")
      class CatAdoptionController(
          private val adoptCatService: AdoptCatService,
          private val removeAdoptionService: RemoveAdoptionService
      ) {
      
          @PatchMapping("/{id}/adoption")
          fun adopt(
              @PathVariable id: Long,
              @Valid @RequestBody request: AdoptCatRequest
          ): ResponseEntity<CatResponse> {
              return ResponseEntity.ok(
                  adoptCatService.execute(id, request)
              )
          }
      
          @DeleteMapping("/{id}/adoption")
          fun removeAdoption(
              @PathVariable id: Long,
              @RequestBody request: RemoveAdoptionRequest
          ): ResponseEntity<CatResponse> {
              return ResponseEntity.ok(
                  removeAdoptionService.execute(id, request)
              )
          }
      }
      
  22. Executar projeto (comando no terminal): .\gradlew bootRun
  23. Testar funcionalidades via request REST no Postman:
    1. Listar gatos (GET): http://localhost:8080/cats
    2. Adicionar gato (POST): http://localhost:8080/cats
      
      Body (raw JSON):
      {
          "name": "Mingau",
          "breed": "Persa",
          "age": 3
      }
      
    3. Visualizar gato específico (GET): http://localhost:8080/cats/1
    4. Editar gato específico (PUT): http://localhost:8080/cats/1
      
      Body (raw JSON):
      {
          "name": "Mingau ATUALIZADO",
          "breed": "Siamês",
          "age": 4
      }
      
    5. Adotar gato específico (PATCH): http://localhost:8080/cats/1/adoption
      
      Body (raw JSON):
      {
          "adopterName": "Mateus"
      }
      
    6. Remover adoção de gato específico (DELETE): http://localhost:8080/cats/1/adoption
      
      Body (raw JSON):
      {
          "reason": "Adotante desistiu da adoção"
      }
      
    7. Remover gato específico (DELETE): http://localhost:8080/cats/1

Elaborado por Mateus Schwede
ubsocial.github.io