🍃☕ 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 HTTPservice
: Lógica de negóciorepository
: Acesso a dadosmodel
/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
- Coesão: Funcionalidades relacionadas ficam próximas
- Facilidade de navegação: Tudo relacionado a um feature está em um lugar
- Facilidade de manutenção: Alterações ficam isoladas
- 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.