El problema no es la cantidad de carpetas
Una estructura puede verse ordenada y aun así esconder un diseño débil. Cuando todos los controladores viven juntos, todos los servicios en otra carpeta y todos los repositorios en una tercera, cada cambio de negocio obliga a recorrer el proyecto horizontalmente.
El problema aparece cuando una feature como autenticación, mantenimiento o procesamiento de video no tiene un límite visible. Sus clases están separadas por tipo técnico, pero mezcladas conceptualmente con todo lo demás.
Prefiero package-by-feature
En sistemas con varios dominios organizo primero por capacidad de negocio y después por responsabilidad interna:
com.cambers.elevideo/
├── auth/
│ ├── api/
│ ├── application/
│ ├── domain/
│ └── infrastructure/
├── projects/
├── videos/
├── jobs/
├── notifications/
└── shared/Esta estructura permite responder preguntas concretas:
No todos los módulos necesitan cuatro capas
La arquitectura debe expresar complejidad real. Un módulo pequeño puede tener un controlador, un servicio de aplicación y un repositorio sin crear abstracciones adicionales. Un módulo con reglas relevantes puede justificar dominio, puertos, adaptadores y eventos internos.
La regla que sigo es sencilla: introduzco una capa cuando protege una decisión o reduce un acoplamiento observable.
Interfaces de servicio: útiles, pero no automáticas
Crear UserService y UserServiceImpl por costumbre añade archivos sin añadir diseño. Una interfaz tiene valor cuando existe al menos una de estas condiciones:
Para una operación interna estable, inyectar directamente una clase concreta puede ser más honesto y simple.
El controlador no define el caso de uso
El controlador traduce HTTP hacia una operación de aplicación. No debe decidir estados del dominio, construir consultas complejas ni coordinar integraciones:
@RestController
@RequestMapping("/api/v1/jobs")
class JobController {
private final CancelJobUseCase cancelJob;
@PostMapping("/{jobId}/cancel")
ResponseEntity<Void> cancel(@PathVariable UUID jobId) {
cancelJob.execute(jobId);
return ResponseEntity.noContent().build();
}
}La operación de aplicación valida identidad, estado actual y reglas de transición. El adaptador HTTP solo convierte entrada y salida.
Monolito modular antes que microservicios prematuros
En Elevideo mantuve el dominio transaccional dentro de Spring Boot y separé únicamente el procesamiento multimedia, porque esa carga tiene dependencias, consumo de CPU y ciclo de despliegue distintos.
En INCACORE, mantenimiento, inventario y embarcaciones permanecen dentro de una sola aplicación porque comparten transacciones y una fuente de datos. Dividirlos antes de necesitar escalado independiente habría aumentado la coordinación distribuida sin resolver un problema real.
Cómo valido un límite modular
Un nombre de paquete no crea arquitectura. Reviso cuatro señales:
Spring Modulith ayuda a documentar y verificar estas relaciones, pero no sustituye el modelado.
Conclusión
Una estructura profesional no es la que tiene más carpetas. Es la que permite entender el negocio, cambiar una capacidad con impacto controlado y explicar por qué cada límite existe.
Mi criterio actual es comenzar con módulos cohesionados dentro de Spring Boot, mantener contratos explícitos y separar un servicio solo cuando su carga o ritmo operativo realmente lo exige.
