image

Acesse bootcamps ilimitados e +650 cursos pra sempre

60
%OFF
Article image
Otávio Guedes
Otávio Guedes15/08/2025 18:52
Compartilhe
Suzano - Python Developer #2Recomendados para vocêSuzano - Python Developer #2

🍃☕ Estruturando Projetos Spring Boot: Do Básico ao Avançado

    A estrutura de um projeto Spring Boot pode fazer a diferença entre uma aplicação fácil de manter e um pesadelo para desenvolvedores. Neste artigo, exploraremos desde a estrutura básica até padrões arquiteturais avançados, passando por boas práticas que facilitarão a evolução e manutenção do seu projeto.

    Estrutura Básica: O Ponto de Partida

    Todo projeto Spring Boot começa com uma estrutura básica gerada pelo Spring Initializr. Vamos entender cada componente:

    my-spring-app/
    ├── src/
    │   ├── main/
    │   │   ├── java/
    │   │   │   └── com/company/app/
    │   │   │       ├── MySpringAppApplication.java
    │   │   │       ├── controller/
    │   │   │       ├── service/
    │   │   │       ├── repository/
    │   │   │       └── model/
    │   │   └── resources/
    │   │       ├── application.yml
    │   │       ├── static/
    │   │       └── templates/
    │   └── test/
    │       └── java/
    └── pom.xml
    

    Componentes Fundamentais

    1. Application Class

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

    2. Estrutura de Pacotes Básica

    • controller: Classes que recebem requisições HTTP
    • service: Lógica de negócio
    • repository: Acesso a dados
    • model/entity: Entidades e DTOs

    Evoluindo para Estruturas Mais Robustas

    Package by Feature vs Package by Layer

    Package by Layer (Tradicional):

    com.company.app/
    ├── controller/
    │   ├── UserController.java
    │   └── ProductController.java
    ├── service/
    │   ├── UserService.java
    │   └── ProductService.java
    ├── repository/
    │   ├── UserRepository.java
    │   └── ProductRepository.java
    └── model/
      ├── User.java
      └── Product.java
    

    Package by Feature (Recomendado):

    com.company.app/
    ├── user/
    │   ├── UserController.java
    │   ├── UserService.java
    │   ├── UserRepository.java
    │   ├── User.java
    │   └── dto/
    │       ├── UserRequestDTO.java
    │       └── UserResponseDTO.java
    ├── product/
    │   ├── ProductController.java
    │   ├── ProductService.java
    │   ├── ProductRepository.java
    │   ├── Product.java
    │   └── dto/
    └── shared/
      ├── config/
      ├── exception/
      └── util/
    

    Vantagens do Package by Feature

    1. Coesão: Funcionalidades relacionadas ficam próximas
    2. Facilidade de navegação: Tudo relacionado a um feature está em um lugar
    3. Facilidade de manutenção: Alterações ficam isoladas
    4. Preparação para microservices: Cada feature pode se tornar um serviço

    Estrutura Avançada: Arquitetura em Camadas

    Para projetos complexos, uma estrutura mais sofisticada se faz necessária:

    com.company.app/
    ├── application/           # Application Layer
    │   ├── controller/
    │   ├── dto/
    │   └── facade/
    ├── domain/               # Domain Layer
    │   ├── model/
    │   ├── service/
    │   └── repository/       # Interfaces
    ├── infrastructure/       # Infrastructure Layer
    │   ├── persistence/
    │   │   ├── entity/
    │   │   ├── repository/   # Implementações
    │   │   └── mapper/
    │   ├── external/
    │   │   ├── client/
    │   │   └── adapter/
    │   └── config/
    └── shared/              # Shared Kernel
      ├── exception/
      ├── util/
      └── constant/
    

    Detalhamento das Camadas

    Application Layer

    @RestController
    @RequestMapping("/api/users")
    public class UserController {
      
      private final UserFacade userFacade;
      
      @PostMapping
      public ResponseEntity<UserResponseDTO> createUser(
              @Valid @RequestBody UserRequestDTO request) {
          UserResponseDTO response = userFacade.createUser(request);
          return ResponseEntity.status(HttpStatus.CREATED).body(response);
      }
    }
    
    @Component
    public class UserFacade {
      
      private final UserService userService;
      private final UserMapper userMapper;
      
      public UserResponseDTO createUser(UserRequestDTO request) {
          User user = userMapper.toEntity(request);
          User savedUser = userService.createUser(user);
          return userMapper.toResponseDTO(savedUser);
      }
    }
    

    Domain Layer

    // Domain Model
    public class User {
      private UserId id;
      private Email email;
      private Name name;
      private UserStatus status;
      
      public void activate() {
          if (this.status == UserStatus.INACTIVE) {
              this.status = UserStatus.ACTIVE;
          } else {
              throw new IllegalStateException("User is already active");
          }
      }
    }
    
    // Domain Service
    @Service
    public class UserService {
      
      private final UserRepository userRepository;
      private final EmailService emailService;
      
      @Transactional
      public User createUser(User user) {
          validateUser(user);
          User savedUser = userRepository.save(user);
          emailService.sendWelcomeEmail(savedUser.getEmail());
          return savedUser;
      }
      
      private void validateUser(User user) {
          if (userRepository.existsByEmail(user.getEmail())) {
              throw new UserAlreadyExistsException("User with email already exists");
          }
      }
    }
    

    Infrastructure Layer

    // JPA Entity
    @Entity
    @Table(name = "users")
    public class UserEntity {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
      
      @Column(unique = true, nullable = false)
      private String email;
      
      private String name;
      
      @Enumerated(EnumType.STRING)
      private UserStatus status;
      
      // getters, setters, constructors
    }
    
    // Repository Implementation
    @Repository
    public class UserRepositoryImpl implements UserRepository {
      
      private final UserJpaRepository jpaRepository;
      private final UserEntityMapper entityMapper;
      
      @Override
      public User save(User user) {
          UserEntity entity = entityMapper.toEntity(user);
          UserEntity saved = jpaRepository.save(entity);
          return entityMapper.toDomain(saved);
      }
      
      @Override
      public boolean existsByEmail(Email email) {
          return jpaRepository.existsByEmail(email.getValue());
      }
    }
    

    Configuração e Profiles

    Estrutura de Configuração

    src/main/resources/
    ├── application.yml                 # Configurações base
    ├── application-dev.yml            # Desenvolvimento
    ├── application-test.yml           # Testes
    ├── application-prod.yml           # Produção
    ├── db/
    │   └── migration/
    │       ├── V1__Create_user_table.sql
    │       └── V2__Add_user_status.sql
    └── static/
      └── docs/
          └── api-docs.yml
    

    Classes de Configuração

    @Configuration
    @EnableJpaRepositories(basePackages = "com.company.app.infrastructure.persistence")
    public class DatabaseConfig {
      
      @Bean
      @Profile("!test")
      public DataSource dataSource() {
          // Configuração do DataSource para produção
          return DataSourceBuilder.create().build();
      }
      
      @Bean
      @Profile("test")
      public DataSource testDataSource() {
          // Configuração para testes (H2, TestContainers, etc.)
          return new EmbeddedDatabaseBuilder()
                  .setType(EmbeddedDatabaseType.H2)
                  .build();
      }
    }
    
    @ConfigurationProperties(prefix = "app.user")
    @ConstructorBinding
    public class UserConfigProperties {
      private final int maxLoginAttempts;
      private final Duration sessionTimeout;
      private final boolean emailVerificationRequired;
      
      // constructor, getters
    }
    

    Tratamento de Exceções Centralizado

    @ControllerAdvice
    public class GlobalExceptionHandler {
      
      private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
      
      @ExceptionHandler(UserAlreadyExistsException.class)
      public ResponseEntity<ErrorResponse> handleUserAlreadyExists(
              UserAlreadyExistsException ex) {
          logger.warn("User already exists: {}", ex.getMessage());
          
          ErrorResponse error = ErrorResponse.builder()
                  .code("USER_ALREADY_EXISTS")
                  .message(ex.getMessage())
                  .timestamp(Instant.now())
                  .build();
                  
          return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
      }
      
      @ExceptionHandler(MethodArgumentNotValidException.class)
      public ResponseEntity<ValidationErrorResponse> handleValidation(
              MethodArgumentNotValidException ex) {
          
          Map<String, String> errors = ex.getBindingResult()
                  .getFieldErrors()
                  .stream()
                  .collect(Collectors.toMap(
                      FieldError::getField,
                      FieldError::getDefaultMessage,
                      (existing, replacement) -> replacement
                  ));
          
          ValidationErrorResponse error = ValidationErrorResponse.builder()
                  .code("VALIDATION_ERROR")
                  .message("Invalid input data")
                  .errors(errors)
                  .timestamp(Instant.now())
                  .build();
                  
          return ResponseEntity.badRequest().body(error);
      }
    }
    

    Testes: Estrutura Espelhada

    src/test/java/
    ├── unit/                          # Testes unitários
    │   ├── domain/
    │   │   └── service/
    │   │       └── UserServiceTest.java
    │   └── application/
    │       └── facade/
    │           └── UserFacadeTest.java
    ├── integration/                   # Testes de integração
    │   ├── controller/
    │   │   └── UserControllerTest.java
    │   └── repository/
    │       └── UserRepositoryTest.java
    ├── e2e/                          # Testes end-to-end
    │   └── UserE2ETest.java
    └── testcontainers/               # Configurações TestContainers
      └── DatabaseTestConfig.java
    

    Exemplo de Teste de Integração

    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    @Testcontainers
    class UserControllerIntegrationTest {
      
      @Container
      static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:14")
              .withDatabaseName("testdb")
              .withUsername("test")
              .withPassword("test");
      
      @Autowired
      private TestRestTemplate restTemplate;
      
      @Autowired
      private UserRepository userRepository;
      
      @Test
      void shouldCreateUserSuccessfully() {
          UserRequestDTO request = UserRequestDTO.builder()
                  .email("test@example.com")
                  .name("Test User")
                  .build();
          
          ResponseEntity<UserResponseDTO> response = restTemplate.postForEntity(
                  "/api/users", request, UserResponseDTO.class);
          
          assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
          assertThat(response.getBody().getEmail()).isEqualTo("test@example.com");
          assertThat(userRepository.count()).isEqualTo(1);
      }
    }
    

    Estrutura para Microservices

    Quando o projeto cresce e precisa ser dividido em microservices:

    company-services/
    ├── user-service/
    │   ├── src/main/java/com/company/user/
    │   ├── Dockerfile
    │   └── pom.xml
    ├── product-service/
    │   ├── src/main/java/com/company/product/
    │   ├── Dockerfile
    │   └── pom.xml
    ├── shared-library/
    │   └── src/main/java/com/company/shared/
    ├── api-gateway/
    │   └── src/main/java/com/company/gateway/
    ├── service-discovery/
    │   └── src/main/java/com/company/discovery/
    ├── docker-compose.yml
    └── k8s/
      ├── user-service.yaml
      ├── product-service.yaml
      └── gateway.yaml
    

    Boas Práticas e Dicas Finais

    1. Convenções de Nomenclatura

    • Packages: minúsculas, separadas por ponto
    • Classes: PascalCase, nomes descritivos
    • Métodos: camelCase, verbos que descrevem ações
    • Constantes: UPPER_SNAKE_CASE

    2. Organização de Dependências

    <dependencies>
      <!-- Spring Boot Starters -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      
      <!-- Database -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-jpa</artifactId>
      </dependency>
      
      <!-- Validation -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-validation</artifactId>
      </dependency>
      
      <!-- Utilities -->
      <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <optional>true</optional>
      </dependency>
      
      <!-- Testing -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
          <scope>test</scope>
      </dependency>
    </dependencies>
    

    3. Documentação da Estrutura

    Crie um README.md na raiz do projeto explicando:

    • Estrutura de packages
    • Convenções adotadas
    • Como executar o projeto
    • Como executar testes
    • Decisões arquiteturais

    4. Ferramentas de Qualidade

    <plugins>
      <plugin>
          <groupId>org.sonarsource.scanner.maven</groupId>
          <artifactId>sonar-maven-plugin</artifactId>
      </plugin>
      
      <plugin>
          <groupId>com.github.spotbugs</groupId>
          <artifactId>spotbugs-maven-plugin</artifactId>
      </plugin>
      
      <plugin>
          <groupId>org.jacoco</groupId>
          <artifactId>jacoco-maven-plugin</artifactId>
      </plugin>
    </plugins>
    

    Conclusão

    Uma estrutura bem pensada é fundamental para o sucesso de qualquer projeto Spring Boot. Começar com o básico e evoluir conforme a necessidade, sempre mantendo princípios como coesão, baixo acoplamento e facilidade de manutenção, garantirá que seu projeto possa crescer de forma sustentável.

    Lembre-se: não existe uma estrutura perfeita para todos os casos. Analise as necessidades específicas do seu projeto, o tamanho da equipe e os requisitos de negócio antes de decidir qual abordagem seguir. O importante é ser consistente e documentar as decisões arquiteturais para facilitar a vida de quem trabalhar no projeto no futuro.

    A jornada de estruturação de um projeto é evolutiva. Comece simples, refatore quando necessário, e sempre mantenha o foco na legibilidade e manutenibilidade do código.

    Compartilhe
    Recomendados para você
    Ri Happy - Front-end do Zero #2
    Avanade - Back-end com .NET e IA
    Akad - Fullstack Developer
    Comentários (0)
    Recomendados para vocêSuzano - Python Developer #2