Type something to search...
Más Allá del `try-catch`: Diseñando un Sistema de Excepciones Robusto y Escalable 🎯

Más Allá del try-catch: Diseñando un Sistema de Excepciones Robusto y Escalable 🎯

En el desarrollo de APIs, el manejo de errores suele ser un ciudadano de segunda clase. Con frecuencia, nos conformamos con respuestas de error genéricas, inconsistentes o, en el peor de los casos, con trazas de stack trace en HTML que no aportan valor al consumidor de la API. Un buen contrato de API no solo define las rutas de éxito, sino que también establece un lenguaje claro y predecible para cuando las cosas van mal.

El objetivo de este artículo es construir, paso a paso, una estrategia de manejo de excepciones que sea robusta, escalable y centralizada. Dejaremos atrás los bloques try-catch dispersos por el código de negocio para dar paso a un sistema que produce respuestas JSON consistentes y enriquecidas para cualquier tipo de error, ya sea una validación de negocio, un recurso no encontrado o un fallo inesperado del sistema. Para ello, nos apoyaremos en principios de diseño sólidos como el Patrón Strategy, el Principio de Abierto/Cerrado y un enfoque que mantiene nuestro dominio limpio de preocupaciones de infraestructura.

Definiendo un Lenguaje Común para el Error

Antes de manejar cualquier error, debemos definir cómo queremos comunicarlo. En lugar de depender de estructuras volátiles como Map<String, Object>, estableceremos un contrato sólido mediante Data Transfer Objects (DTOs). Esto nos proporciona seguridad de tipos, autocompletado en el IDE y una excelente base para la documentación automática con herramientas como OpenAPI.

Nuestra estructura de respuesta de error estándar será la siguiente:

{
  "meta": {
    "timestamp": "2025-08-28T19:12:58.123Z",
    "path": "/api/users",
    "status": 409,
    "requestId": "a1b2c3d4e5"
  },
  "errors": [
    {
      "code": "409-001",
      "message": "EMAIL ALREADY EXISTS",
      "payload": {
        "email": "[email protected]"
      }
    }
  ]
}

Para modelar esto, definimos tres clases principales. ApiErrorResponse es el contenedor principal, que incluye una sección de metadatos (Meta) y una lista de errores. El ApiError en sí mismo es flexible, con un código, un mensaje y un payload opcional para datos contextuales.

// Modelo de respuesta genérico y estandarizado
@Data
@Builder
public class ApiErrorResponse<T> {
    private Meta meta;
    private List<T> errors;

    @Data
    @Builder
    public static class Meta {
        private String timestamp;
        private String path;
        private int status;
        private String requestId;
    }
}
// DTO que representa un único error de la API
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ApiError(String code, String message, Object payload) {
    public ApiError(String code, String message) {
        this(code, message, null);
    }
}

Finalmente, un pequeño DTO para encapsular el contexto de la petición que se pasará a través de nuestro sistema.

// Encapsula la información del contexto de la request
@Data
@Builder
public class RequestContextApi {
    private String path;
    private String requestId;
}

Manteniendo el Dominio Puro: La BusinessException

Una de las claves de una buena arquitectura es la separación de conceptos. La lógica de negocio no debería saber nada sobre códigos de estado HTTP o la estructura de una respuesta JSON. Para lograrlo, definimos una excepción base para nuestro dominio, BusinessException.

Esta clase abstracta es simple pero poderosa. Contiene un errorCode único para la aplicación y un payload opcional. Cualquier excepción de negocio específica (ej. InsufficientFundsException) heredará de ella, manteniendo el dominio completamente agnóstico a la tecnología.

@Getter
public abstract class BusinessException extends RuntimeException {

    private final String errorCode;
    private final Object payload;

    protected BusinessException(String message, String errorCode, Object payload) {
        super((message != null) ? message.toUpperCase() : "");
        this.errorCode = errorCode;
        this.payload = payload;
    }

    protected BusinessException(String message, String errorCode) {
        super((message != null) ? message.toUpperCase() : "");
        this.errorCode = errorCode;
        this.payload = null;
    }
}

El Cerebro de la Operación: El Patrón Strategy

Con los modelos definidos, es hora de diseñar el mecanismo central. En lugar de un gran bloque if-else o un switch para manejar diferentes tipos de excepciones, utilizaremos el Patrón Strategy. Esto nos permitirá encapsular la lógica para manejar cada tipo de excepción en su propia clase, haciendo el sistema increíblemente fácil de extender.

La piedra angular es la interfaz ExceptionHandlerStrategy. Define un contrato que cada manejador debe cumplir:

  1. supports(Class<? extends Throwable> exceptionType): Determina si el manejador es capaz de procesar un tipo de excepción dado.
  2. getStatus(Throwable ex): Define el HttpStatus que corresponde a la excepción. Esto nos permite devolver códigos más precisos que un simple 400 o 500.
  3. handle(Throwable ex, ...): El método que procesa la excepción. Lo interesante aquí es que proveemos una implementación default que cubre los casos más comunes, de modo que muchos de nuestros manejadores serán puramente declarativos.
public interface ExceptionHandlerStrategy {

    boolean supports(Class<? extends Throwable> exceptionType);

    default HttpStatus getStatus(Throwable ex) {
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }

    default ResponseEntity<ApiErrorResponse<?>> handle(Throwable ex, RequestContextApi context) {
        HttpStatus status = getStatus(ex);
        Object error = new ApiError(String.valueOf(status.value()), "INTERNAL_SERVER_ERROR");
        return buildErrorResponse(status, context, (error instanceof List<?>) ? (List<?>) error : List.of(error));
    }
    
    static ResponseEntity<ApiErrorResponse<?>> buildErrorResponse(
            HttpStatus status, RequestContextApi context, List<?> errors) {
        // ... Lógica para construir la respuesta final ...
    }
}

Estrategias en Acción: El Manejador Específico y el Genérico

Con la interfaz lista, crear manejadores es trivial. Para nuestras BusinessException, creamos un BusinessExceptionHandler. Este manejador sobreescribe getStatus para implementar una lógica ingeniosa que deriva el código de estado HTTP a partir del errorCode de la excepción (ej. “409-001” se convierte en HttpStatus.CONFLICT). También sobreescribe handle para asegurarse de que el payload se incluya en la respuesta.

@Component
public class BusinessExceptionHandler implements ExceptionHandlerStrategy {

    @Override
    public boolean supports(Class<? extends Throwable> exceptionType) {
        return BusinessException.class.isAssignableFrom(exceptionType);
    }

    @Override
    public HttpStatus getStatus(Throwable ex) {
        BusinessException businessException = (BusinessException) ex;
        String codeHttp = businessException.getErrorCode().split("-")[0];
        try {
            int codigo = Integer.parseInt(codeHttp);
            return HttpStatus.valueOf(codigo);
        } catch (Exception e) {
            return HttpStatus.BAD_REQUEST;
        }
    }

    @Override
    public ResponseEntity<ApiErrorResponse<?>> handle(Throwable ex, RequestContextApi context) {
        BusinessException exception = (BusinessException) ex;
        HttpStatus status = getStatus(exception);
        Object error = new ApiError(exception.getErrorCode(), exception.getMessage(), exception.getPayload());
        return ExceptionHandlerStrategy.buildErrorResponse(status, context, (error instanceof List<?>) ? (List<?>) error : List.of(error));
    }
}

Para cualquier otra excepción no controlada, tenemos el GenericExceptionStrategyHandler. Gracias a la anotación @Order(Ordered.LOWEST_PRECEDENCE) de Spring, esta estrategia solo se ejecutará si ninguna otra más específica puede manejar la excepción. Es nuestra red de seguridad, y gracias a la implementación default de la interfaz, su código es mínimo.

@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class GenericExceptionStrategyHandler implements ExceptionHandlerStrategy {

    @Override
    public boolean supports(Class<? extends Throwable> exceptionType) {
        return true;
    }
}

El Orquestador: Poniendo Todo en Marcha

Las estrategias individuales son útiles, pero necesitamos un director de orquesta. Aquí es donde entran el GlobalExceptionHandlerStrategyRegistry y el GlobalExceptionTranslator.

El Registry es una clase simple que se inyecta con una lista de todas las implementaciones de ExceptionHandlerStrategy disponibles en el contexto de Spring. Su única misión es iterar sobre ellas (respetando el @Order) y delegar el control a la primera que declare que puede manejar la excepción.

@Component
public class GlobalExceptionHandlerStrategyRegistry {

    private final List<ExceptionHandlerStrategy> strategies;

    public GlobalExceptionHandlerStrategyRegistry(List<ExceptionHandlerStrategy> strategies) {
        this.strategies = strategies;
    }

    public ResponseEntity<ApiErrorResponse<?>> handle(Throwable ex, RequestContextApi context) {
        return strategies.stream()
                .filter(s -> s.supports(ex.getClass()))
                .findFirst()
                .map(s -> s.handle(ex, context))
                .orElseThrow(() -> new IllegalStateException("No suitable exception handler found.", ex));
    }
}

Finalmente, el Translator es el punto de entrada. Es una clase anotada con @RestControllerAdvice que captura cualquier Throwable que escape de nuestros controladores. Su responsabilidad es mínima y crucial: crear el RequestContextApi y pasarle la excepción al Registry. No contiene ninguna lógica de negocio, lo que lo mantiene limpio y enfocado.

@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionTranslator {

    private final GlobalExceptionHandlerStrategyRegistry registry;

    @ExceptionHandler(Throwable.class)
    public final ResponseEntity<ApiErrorResponse<?>> handleAnyException(
            Throwable ex, ServerWebExchange exchange) {

        RequestContextApi context = RequestContextApi.builder()
                .path(exchange.getRequest().getURI().getPath())
                .requestId(UUID.randomUUID().toString().substring(0, 10))
                .build();

        return registry.handle(ex, context);
    }
}

Con todas las piezas en su lugar, podemos visualizar la arquitectura completa y el flujo de una excepción a través de nuestro sistema. El siguiente diagrama ilustra cómo estos componentes colaboran, desde la captura inicial hasta la selección de la estrategia adecuada y la construcción de la respuesta final.

image

codigo mermaid

        classDiagram
            class DatabaseConnectionPool {
                -DatabaseEngineFactory engineFactory
                +dataSourceFromSecret(String, DatabaseConnectionPropertiesFactory) DataSource
            }
            class DatabaseConnectionPropertiesFactory {
                -GenericManager secretsManager
                +getDatabaseConnectionProperties(String) DatabaseConnectionProperties
            }
            class DatabaseConnectionProperties {
                -String dbname
                -String username
                -String password
                -String engine
                ...
            }
            class DatabaseEngineFactory {
                -Map~String, DatabaseEngineStrategy~ strategies
                +getStrategy(String) DatabaseEngineStrategy
            }
            class DatabaseEngineStrategy {
                <>
                +getName() String
                +buildJdbcUrl(...) String
                +getValidationQuery() String
            }
            class PostgresqlStrategy {
                +getName() String
                ...
            }
            class MysqlStrategy {
                +getName() String
                ...
            }
            DatabaseConnectionPool ..> DatabaseConnectionPropertiesFactory : uses
            DatabaseConnectionPool ..> DatabaseEngineFactory : uses
            DatabaseConnectionPropertiesFactory ..> DatabaseConnectionProperties : creates
            DatabaseEngineFactory ..> DatabaseEngineStrategy : uses
            PostgresqlStrategy --|> DatabaseEngineStrategy : implements
            MysqlStrategy --|> DatabaseEngineStrategy : implements
  

Conclusión y Futuras Mejoras

Hemos construido un sistema de manejo de excepciones que es a la vez potente y elegante. Todas las respuestas de error de nuestra API son ahora consistentes, informativas y se generan a través de un flujo centralizado y predecible. La belleza de este diseño radica en su escalabilidad: añadir soporte para un nuevo tipo de excepción es tan simple como crear una nueva clase Strategy, sin necesidad de modificar el código existente, adhiriéndonos así al Principio de Abierto/Cerrado.

Este sistema, sin embargo, es una base sólida sobre la cual se puede seguir construyendo. Algunas líneas futuras de mejora podrían incluir:

  • Integración con Logging: Centralizar el registro de las excepciones completas dentro de los manejadores para un monitoreo más efectivo.
  • Internacionalización (i18n): Modificar el ApiError y los manejadores para que puedan devolver mensajes de error en diferentes idiomas según las cabeceras de la petición.
  • Manejadores Específicos de Framework: Crear estrategias para excepciones comunes de frameworks como Spring Security (ej. AccessDeniedException) para traducirlas a respuestas 403 Forbidden con un formato consistente.

Related Posts

Diseñando un Wrapper de Respuesta en Java con Funcionalidades de Optional y Gestión de Estado

Diseñando un Wrapper de Respuesta en Java con Funcionalidades de Optional y Gestión de Estado

En el desarrollo de aplicaciones Java, el manejo de respuestas a solicitudes —especialmente aquellas que involucran operaciones asincrónicas, procesamiento de datos o comunicación con servicios extern

Leer más
Una Propuesta para Estandarizar la Seguridad en APIs REST con Arquitectura Hexagonal y Spring Security

Una Propuesta para Estandarizar la Seguridad en APIs REST con Arquitectura Hexagonal y Spring Security

En el desarrollo de aplicaciones empresariales modernas, la seguridad es un pilar fundamental. Sin embargo, lograr una arquitectura de seguridad que sea reutilizable, desacoplada y, al mismo t

Leer más
El Arte de Conectar: Forjando un DataSource Dinámico en Spring Boot

El Arte de Conectar: Forjando un DataSource Dinámico en Spring Boot

En el vertiginoso universo del desarrollo de software, donde la seguridad es un pilar innegociable y la agilidad es la moneda de cambio, nos enfrentamos a desafíos que van más allá de la simple lógica

Leer más
Manejando el Caos: Una Guía Definitiva sobre Excepciones de Dominio 🎯

Manejando el Caos: Una Guía Definitiva sobre Excepciones de Dominio 🎯

En el desarrollo de software moderno, escribir código que funciona perfectamente en escenarios ideales es solo el primer paso. La verdadera fortaleza de un sistema se manifiesta cuando debe enfrentar

Leer más
El Arte del Contexto: Diseño Flexible en Arquitecturas DDD con Java y Spring Boot

El Arte del Contexto: Diseño Flexible en Arquitecturas DDD con Java y Spring Boot

En el universo del desarrollo de software empresarial, nos enfrentamos a un dilema constante: cómo manejar información transversal —ese rastro de datos vitales como IDs de correlación, información del

Leer más
El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC

El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC

El Arte de la Consistencia: Creando Respuestas API Predecibles en Spring MVC Imagina a un desarrollador frontend consumiendo tu API. En un endpoint, recibe un objeto JSON. En otro, una simple lista. S

Leer más
Respuestas API Consistentes: Un Wrapper Transversal con Spring WebFlux y `WebFilter`

Respuestas API Consistentes: Un Wrapper Transversal con Spring WebFlux y `WebFilter`

En entornos modernos de microservicios, la consistencia en las respuestas de una API es más que una cuestión de estética: es un factor crítico para la mantenibilidad, observabilidad y experiencia del

Leer más
Convención de Nombres en Clases Java: Mejora de Legibilidad y Mantenibilidad

Convención de Nombres en Clases Java: Mejora de Legibilidad y Mantenibilidad

En proyectos de gran escala, identificar rápidamente el tipo de una clase facilita la comprensión del código, la colaboración y la depuración. Sin un esquema de nombres estandarizado, es común que los

Leer más
Estructuración de Carpetas en Proyectos de Software

Estructuración de Carpetas en Proyectos de Software

En proyectos de software de gran escala, la falta de una estructura de carpetas bien definida puede generar desorden, dificultando la mantenibilidad, escalabilidad y comprensión del código. Muchas vec

Leer más
Desarrollo de Software Implementando Gitflow

Desarrollo de Software Implementando Gitflow

Introducción El desarrollo de software requiere metodologías y flujos de trabajo que permitan un control eficiente del código fuente. Uno de los enfoques más utilizados para la gestión de versiones

Leer más
Transformando Colecciones con Java Streams: 15 Métodos Esenciales

Transformando Colecciones con Java Streams: 15 Métodos Esenciales

Introducción En el mundo de Java, trabajar con colecciones de datos solía ser sinónimo de bucles interminables, condicionales anidados y código repetitivo. Pero con la llegada de Java Streams

Leer más
Optimizando el Acceso a Datos: La Importancia de las Proyecciones JPA en Spring Boot

Optimizando el Acceso a Datos: La Importancia de las Proyecciones JPA en Spring Boot

El Costo Oculto de Traer Demasiada Información En el desarrollo de aplicaciones que interactúan con bases de datos, una tarea fundamental es la recuperación de datos. Al usar Object-Relational Map

Leer más
Implementando Trunk Based Development con Ramas de Vida Corta en Tu Equipo: Una Guía Completa

Implementando Trunk Based Development con Ramas de Vida Corta en Tu Equipo: Una Guía Completa

En el ámbito del desarrollo de software, la elección de una estrategia de versionamiento eficiente y estable es fundamental para el éxito de un equipo. El Trunk Based Development (TBD) tradicional, do

Leer más
Estándar de Codificación y Gestión de Errores en Arquitecturas de Microservicios

Estándar de Codificación y Gestión de Errores en Arquitecturas de Microservicios

En el ecosistema de aplicaciones web modernas, la arquitectura de microservicios distribuidos se ha consolidado como un paradigma dominante. Su flexibilidad, escalabilidad y resiliencia son innegables

Leer más
Desmitificando Gradle: El Primer Paso para Automatizar tu Mundo Java

Desmitificando Gradle: El Primer Paso para Automatizar tu Mundo Java

En el vertiginoso universo del desarrollo de software, la eficiencia no es un lujo, es una necesidad. Dedicar tiempo a tareas repetitivas como compilar código, ejecutar pruebas, empaquetar la aplicaci

Leer más
Del Dicho al Hecho: Generando Proyectos Java con Plantillas y FreeMarker

Del Dicho al Hecho: Generando Proyectos Java con Plantillas y FreeMarker

En el artículo anterior, alcanzamos un hito crucial: construimos un plugin binario funcional en Java, completo con su propia configuración y tarea. Nuestro plugin "saludador" demostró que dominamos la

Leer más
De Consumidor a Creador: Construyendo tu Primer Plugin Binario de Gradle

De Consumidor a Creador: Construyendo tu Primer Plugin Binario de Gradle

En nuestro artículo anterior, desmitificamos Gradle y sentamos las bases para entender su funcionamiento. Aprendimos a crear proyectos, ejecutar tareas y comprendimos el rol fundamental de los plugins

Leer más
Arquitectura DDD y Hexagonal: Construyendo Software para el Futuro

Arquitectura DDD y Hexagonal: Construyendo Software para el Futuro

En el dinámico mundo del desarrollo de software, la complejidad es el enemigo silencioso. Las aplicaciones crecen, los requisitos cambian y, sin una guía clara, el código puede convertirse rápidamente

Leer más