Estrutura e Padrões - Backend FinBoost+
Visão Geral da Arquitetura
O backend do FinBoost+ é construído com Spring Boot 3.5+ e Java 21, seguindo uma arquitetura limpa em camadas baseada nos padrões Domain Driven Design (DDD) e Clean Architecture. A API REST fornece endpoints seguros para gestão financeira colaborativa.
Princípios Arquiteturais
- Separation of Concerns: Cada camada tem responsabilidade específica
- Domain-First: Domínio no centro, independente de frameworks
- RESTful Design: APIs seguem princípios REST
- Security by Design: Autenticação JWT e autorização por roles
- Database-Agnostic: JPA permite trocar banco facilmente
- Test-Driven: Cobertura > 70% com testes automatizados
Stack Tecnológica
Core Framework
- Java 21: Records, Pattern Matching, Virtual Threads
- Spring Boot 3.5: Framework principal com auto-configuração
- Spring Data JPA: Abstração de persistência sobre Hibernate
- Spring Security: Autenticação JWT e controle de acesso
- Spring Web MVC: Controllers REST com Jackson
Banco de Dados & Segurança
- PostgreSQL 15+: Banco relacional principal
- H2: Banco em memória para testes
- OAuth2 + JWT: Servidor de autorização próprio
- BCrypt: Hash de senhas
Documentação & Qualidade
- SpringDoc OpenAPI 3: Documentação automática
- Swagger UI / Scalar: Interface interativa para testes
- JUnit 5 + Mockito: Testes unitários e integração
- JaCoCo: Cobertura de código
Estrutura de Pastas
src/main/java/com/finboostplus/
├── FinboostplusApplication.java # Classe principal
├──
├── config/ # Configurações do Spring
│ ├── SecurityConfig.java # Configuração de segurança
│ ├── CorsConfig.java # Configuração CORS
│ ├── OpenApiConfig.java # Configuração Swagger/OpenAPI
│ └── DatabaseConfig.java # Configuração JPA/DataSource
│
├── controller/ # Camada de Apresentação (REST)
│ ├── UserController.java # Endpoints de usuários
│ ├── GroupController.java # Endpoints de grupos
│ ├── ExpenseController.java # Endpoints de despesas
│ └── AuthController.java # Autenticação e registro
│
├── service/ # Camada de Negócio
│ ├── UserService.java # Lógica de usuários
│ ├── GroupService.java # Lógica de grupos
│ ├── ExpenseService.java # Lógica de despesas
│ └── impl/ # Implementações dos serviços
│
├── repository/ # Camada de Dados (JPA)
│ ├── UserRepository.java # Acesso a dados de usuários
│ ├── GroupRepository.java # Acesso a dados de grupos
│ └── ExpenseRepository.java # Acesso a dados de despesas
│
├── model/ # Entidades do Domínio
│ ├── User.java # Entidade Usuário
│ ├── Group.java # Entidade Grupo
│ ├── Expense.java # Entidade Despesa
│ ├── Category.java # Entidade Categoria
│ └── GroupMember.java # Relacionamento Grupo-Usuário
│
├── dto/ # Data Transfer Objects
│ ├── request/ # DTOs de entrada
│ └── response/ # DTOs de saída
│
├── exception/ # Tratamento de Exceções
│ ├── GlobalExceptionHandler.java # Handler global
│ └── BusinessException.java # Exceções de negócio
│
└── util/ # Utilitários
├── Constants.java # Constantes da aplicação
└── SecurityUtils.java # Utilidades de segurança
Padrões de Desenvolvimento
Arquitetura em Camadas
@RestController
@RequestMapping("/api/users")
@Tag(name = "Usuários", description = "Gestão de usuários")
public class UserController {
private final UserService userService;
@PostMapping
@Operation(summary = "Criar usuário")
public ResponseEntity<UserResponseDTO> create(
@Valid @RequestBody UserCreateDTO request) {
var user = userService.create(request);
return ResponseEntity.status(201).body(user);
}
}
@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserResponseDTO create(UserCreateDTO dto) {
validateEmailNotExists(dto.getEmail());
var user = User.builder()
.name(dto.getName())
.email(dto.getEmail())
.password(passwordEncoder.encode(dto.getPassword()))
.build();
var saved = userRepository.save(user);
return UserResponseDTO.from(saved);
}
}
DTOs com Records
// Request DTO
public record UserCreateDTO(
@NotBlank @Size(min = 2, max = 100) String name,
@NotBlank @Email String email,
@NotBlank @Size(min = 6) String password
) {}
// Response DTO
public record UserResponseDTO(
Long id,
String name,
String email,
Instant createdAt,
Set<String> roles
) {
public static UserResponseDTO from(User user) {
return new UserResponseDTO(
user.getId(),
user.getName(),
user.getEmail(),
user.getCreatedAt(),
user.getRoles().stream()
.map(Role::getName)
.collect(Collectors.toSet())
);
}
}
Entidades JPA
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_name", nullable = false)
private String name;
@Column(name = "e_mail", nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@CreationTimestamp
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "users_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
@Builder.Default
private Set<Role> roles = new HashSet<>();
// UserDetails implementation methods...
}
Configurações Principais
Spring Security
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/docs/**", "/swagger-ui/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtDecoder(jwtDecoder())))
.build();
}
}
Tratamento Global de Erros
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
var error = ErrorResponse.builder()
.message(e.getMessage())
.code("BUSINESS_ERROR")
.timestamp(Instant.now())
.build();
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidation(
MethodArgumentNotValidException e) {
var errors = e.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage));
var response = ValidationErrorResponse.builder()
.message("Dados inválidos")
.errors(errors)
.timestamp(Instant.now())
.build();
return ResponseEntity.badRequest().body(response);
}
}
API REST - Convenções
Endpoints Padrão
Método | Endpoint | Descrição | Status |
---|---|---|---|
GET | /api/users |
Listar usuários | 200 |
GET | /api/users/{id} |
Buscar por ID | 200, 404 |
POST | /api/users |
Criar usuário | 201, 400 |
PUT | /api/users/{id} |
Atualizar completo | 200, 404 |
PATCH | /api/users/{id} |
Atualização parcial | 200, 404 |
DELETE | /api/users/{id} |
Excluir usuário | 204, 404 |
Códigos de Status HTTP
Status Codes Principais
- 200 OK - Operação bem-sucedida
- 201 Created - Recurso criado
- 204 No Content - Operação sem retorno
- 400 Bad Request - Dados inválidos
- 401 Unauthorized - Não autenticado
- 403 Forbidden - Não autorizado
- 404 Not Found - Recurso não encontrado
- 409 Conflict - Conflito (email duplicado)
Paginação
@GetMapping
public ResponseEntity<Page<UserResponseDTO>> findAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "name") String sort,
@RequestParam(required = false) String search) {
var pageable = PageRequest.of(page, size,
Sort.by(Sort.Direction.ASC, sort));
var users = userService.findAll(pageable, search);
return ResponseEntity.ok(users);
}
Performance & Otimizações
Queries Otimizadas
@Repository
public interface GroupRepository extends JpaRepository<Group, Long> {
// Query com JOIN FETCH para evitar N+1
@Query("SELECT g FROM Group g JOIN FETCH g.members m WHERE m.user.id = :userId")
List<Group> findByUserIdWithMembers(Long userId);
// Query nativa para agregações complexas
@Query(value = """
SELECT g.id, g.name, COUNT(gm.user_id) as member_count,
COALESCE(SUM(e.amount), 0) as total_expenses
FROM groups g
LEFT JOIN group_members gm ON g.id = gm.group_id
LEFT JOIN expenses e ON g.id = e.group_id
WHERE gm.user_id = :userId
GROUP BY g.id, g.name
""", nativeQuery = true)
List<GroupSummaryProjection> findGroupSummaryByUserId(Long userId);
}
Cache Configuration
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users", "groups", "categories");
}
}
// Uso em Service
@Cacheable(value = "users", key = "#id")
public UserResponseDTO findById(Long id) {
return userRepository.findById(id)
.map(UserResponseDTO::from)
.orElseThrow(() -> new ResourceNotFoundException("Usuário não encontrado"));
}
Configurações de Ambiente
Application Properties
Documentação da API
OpenAPI Configuration
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("FinBoost+ API")
.version("1.0")
.description("API REST para gestão financeira colaborativa"))
.addSecurityItem(new SecurityRequirement()
.addList("Bearer Authentication"))
.components(new Components()
.addSecuritySchemes("Bearer Authentication",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
Controller Documentation
@Tag(name = "Grupos", description = "Gestão de grupos financeiros")
@RestController
@RequestMapping("/api/groups")
public class GroupController {
@Operation(
summary = "Criar grupo",
description = "Cria um novo grupo financeiro e adiciona o usuário como administrador"
)
@ApiResponses({
@ApiResponse(responseCode = "201", description = "Grupo criado com sucesso"),
@ApiResponse(responseCode = "400", description = "Dados inválidos"),
@ApiResponse(responseCode = "401", description = "Usuário não autenticado")
})
@PostMapping
public ResponseEntity<GroupResponseDTO> create(
@Valid @RequestBody GroupCreateDTO request,
@AuthenticationPrincipal UserDetails currentUser) {
var group = groupService.create(request, currentUser.getUsername());
return ResponseEntity.status(201).body(group);
}
}
Deploy com Docker
Dockerfile
FROM openjdk:21-jdk-slim
WORKDIR /app
COPY target/finboostplus-server-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Docker Compose
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DATABASE_URL=jdbc:postgresql://db:5432/finboost
- DATABASE_USERNAME=finboost
- DATABASE_PASSWORD=prod123
depends_on:
- db
db:
image: postgres:15
environment:
- POSTGRES_DB=finboost
- POSTGRES_USER=finboost
- POSTGRES_PASSWORD=prod123
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Resumo
Esta estrutura garante escalabilidade, manutenibilidade e performance para o backend do FinBoost+, seguindo as melhores práticas de desenvolvimento Spring Boot e arquitetura em camadas.