Spring BootSpring ModulithArchitectureJava

Cómo organizo un backend Spring Boot por dominio, no por carpetas

Una estructura profesional debe hacer visibles los límites del negocio. Explico cuándo usar paquetes por feature, cuándo introducir interfaces y cómo evitar un monolito accidentalmente acoplado.

Edgar Camberos
Edgar Camberos
25 jul 202611 min de lectura

Código y evidencia relacionada

Enlaces directos a implementaciones, repositorios o recursos desplegados.

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:

textedgar.dev
com.cambers.elevideo/
├── auth/
│   ├── api/
│   ├── application/
│   ├── domain/
│   └── infrastructure/
├── projects/
├── videos/
├── jobs/
├── notifications/
└── shared/

Esta estructura permite responder preguntas concretas:

  • ¿Dónde está la regla que autoriza cancelar un job?
  • ¿Qué módulo es propietario del estado de procesamiento?
  • ¿Qué dependencia necesita realmente autenticación?
  • ¿Qué parte podría extraerse sin arrastrar medio sistema?
  • 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:

  • Hay más de una implementación plausible.
  • El módulo expone un contrato a otros módulos.
  • Se necesita aislar una integración externa.
  • La abstracción representa una capacidad del dominio, no solo una clase duplicada.
  • 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:

    javaedgar.dev
    @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:

  • Propiedad de datos: qué módulo puede modificar cada agregado.
  • Dirección de dependencias: quién conoce a quién.
  • Límite transaccional: qué debe confirmarse de forma atómica.
  • Contrato: qué operación puede consumir otro módulo sin acceder a detalles internos.
  • 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.

    Edgar Camberos

    Java Backend Developer · Spring Boot