Spring BootClean ArchitectureJava

Cómo estructuro un backend profesional con Spring Boot

La estructura de un proyecto no es solo organización de carpetas. Es la primera decisión arquitectónica que tomamos y define cómo evoluciona el sistema.

Edgar
Edgar Camberos
2025-05-108 min de lectura

El problema con la estructura por tipo

Organizar el proyecto poniendo todos los controladores juntos, todos los servicios juntos, todos los repositorios juntos funciona en proyectos pequeños. En proyectos reales, navegar para entender qué hace una sola feature implica saltar entre carpetas. La cohesión está fragmentada.

La estructura que uso

Prefiero una estructura que refleje la separación de responsabilidades y sea clara para cualquier miembro del equipo:

src/
└── com/dominio/app/
    ├── config/           # Configuraciones (Security, CORS, Swagger)
    ├── controllers/      # Capa de entrada HTTP
    ├── services/
    │   ├── interfaces/   # Contratos del dominio
    │   └── impl/         # Implementaciones
    ├── repositories/     # Acceso a datos (JPA)
    ├── models/
    │   ├── entities/     # Entidades JPA
    │   ├── dtos/
    │   │   ├── request/  # DTOs de entrada
    │   │   └── response/ # DTOs de salida
    │   └── enums/
    └── exceptions/       # Manejo global de errores

Por qué importa la separación service/impl

Separar interfaz e implementación puede ser útil cuando el dominio necesita más de una implementación o pruebas aisladas. No es una regla universal; en ese contexto permite:

  • Testear en aislamiento — inyectar un mock limpio en los tests de controlador
  • Invertir dependencias — el controlador depende de la abstracción, no de la implementación
  • Flexibilidad real — cambiar la implementación sin tocar el código que la consume
  • @RestController
    @RequestMapping("/api/users")
    public class UserController {
        private final UserService userService; // interfaz, no impl
        
        @GetMapping("/{id}")
        public ResponseEntity<UserResponseDto> getUser(@PathVariable Long id) {
            return ResponseEntity.ok(userService.findById(id));
        }
    }

    El manejo global de excepciones

    Un @ControllerAdvice centralizado es no-negociable. Sin él, cada controlador maneja errores de manera diferente, el cliente recibe formatos inconsistentes y el código se llena de try-catch redundantes.

    @RestControllerAdvice
    public class GlobalExceptionHandler {
        
        @ExceptionHandler(ResourceNotFoundException.class)
        public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
        }
    }

    Conclusión

    La estructura de un proyecto es la primera forma en que comunicas tus intenciones arquitectónicas al equipo. Una buena estructura te da navegación intuitiva, separación de responsabilidades clara y base para escalar sin refactorizar todo.

    Edgar Camberos

    Java Backend Developer · Spring Boot

    Edgar Camberos | Java Backend Developer · Spring Boot