- Lenguaje: Java 21
- Framework: Spring Boot 4.0.0
- Build Tool: Maven (con Maven wrapper
./mvnw) - Base de Datos: PostgreSQL con Flyway migrations
- Arquitectura: Arquitectura Hexagonal (Domain-Driven Design with ports/adapters)
./mvnw clean package # Compilar la aplicación
./mvnw spring-boot:run # Ejecutar la aplicación./mvnw test # Ejecutar todos los tests
./mvnw test -Dtest=ClassName # Ejecutar clase de test específica
./mvnw test -Dtest=ClassName#methodName # Ejecutar método de test específico
./mvnw test -Dtest="ClassName,ClassName2" # Ejecutar múltiples clases de test./mvnw compile # Compilar sin ejecutar tests
./mvnw dependency:tree # Mostrar árbol de dependenciassrc/main/java/com/forsoftwaredevelopers/audio_stream_api/
├── application/
│ ├── command/ # Command DTOs (Java records)
│ ├── service/ # Implementaciones de servicios
│ └── usecase/ # Interfaces de casos de uso
├── domain/
│ ├── model/ # Entidades de dominio
│ ├── port/ # Interfaces de puertos (repositorio, almacenamiento)
│ └── result/ # Tipo Result<T> para manejo de errores
└── infrastructure/
├── audio/ # Implementaciones de almacenamiento de audio
├── config/ # Propiedades de configuración
├── persistense/ # Entidades JPA, repositorios, mappers
└── web/ # Controladores REST
- Use Cases:
XxxUseCase(interfaz) - Commands:
XxxCommand(Java records para DTOs inmutables) - Services:
XxxService(implementación) - Controllers:
XxxControlleroXxxMessageController - JPA Entities:
XxxJPAEntity - Ports:
XxxPort(interfaz) - Repository Adapters:
XxxRepositoryAdapter - Mappers:
XxxJpaMapper
- Usar la interfaz sealed
Result<T>para manejo explícito de errores - Crear
DomainErrorcon código de error y tipo de error (VALIDATION, CONFLICT, NOT_FOUND) - Siempre retornar
Result<T>desde métodos de use case - Usar
result.isFail(),result.getOrThrow(),result.propagate()
Example:
public Result<Void> play(PlayVoiceMessageCommand command) {
VoiceMessage voiceMessage = voiceMessageRepository.findById(command.voiceMessageId());
Result<Void> result = voiceMessage.markAsPlayed();
if (result.isFail()) {
return result.propagate();
}
voiceMessageRepository.save(voiceMessage);
return Result.ok(null);
}- Organizar imports: java., org.springframework., paquetes domain, paquetes application, paquetes infrastructure
- Usar nombres completamente calificados cuando sea inequívoco para reducir clutter
- Usar 4 espacios para indentación (no tabs)
- Llave de apertura en la misma línea (
{) - Longitud máxima de línea: 120 caracteres (guía suave)
- Usar
varpara inferencia de tipo de variable local cuando el tipo sea obvio - Usar Java records para DTOs/commands inmutables
- Usar
Stringpara IDs (generados viaUUID.randomUUID().toString()) - Usar
java.time.Instantpara timestamps - Usar
java.util.Listpara colecciones - Usar interfaces sealed para tipos union (ej.,
Result<T>)
- Dependency Injection: Usar inyección por constructor (no inyección por campo)
- Inmutabilidad: Usar Java records para commands y DTOs
- Validación: Realizar validación en modelos de dominio usando métodos estáticos factory
- Database Migrations: Usar Flyway para migraciones de schema (agregar archivos SQL en
src/main/resources/db/migration/) - Configuración de Propiedades: Usar
@ConfigurationPropertiespara configuración tipada
Capas de testing (3 niveles):
- Pruebas Unitarias (
domain/): Testean entidades y lógica de dominio sin dependencias externas. Usar solo JUnit y Mockito. - Pruebas de Integración (
application/yinfrastructure/): Testean servicios y repositorios con base de datos en memoria (H2). Usar@SpringBootTestcon perfil test. - Pruebas E2E (
integration/): Testean la API completa simulando llamadas HTTP reales.
Las clases de test deben estar en src/test/java/ reflejando la estructura de paquetes del main
- Ubicar mappers en
infraestructure.persistense.mapper - Usar anotación
@Mapper(componentModel = "spring") - MapStruct genera implementación en tiempo de compilación
Creando Domain Objects:
public static Result<VoiceMessage> create(String streamId, String username, String email) {
var validationResult = validate(streamId, username, email);
if (validationResult.isFail()) {
return validationResult.propagate();
}
return Result.ok(new VoiceMessage(...));
}Private Constructors with Factory Methods:
private VoiceMessage(...) {
// initialization
}
public static Result<VoiceMessage> create(...) { ... }
public static VoiceMessage restore(...) { ... } # para reconstrucción JPA00-architecture-decisions.md