Type something to search...
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 situaciones imprevistas, errores y desviaciones del comportamiento esperado. En este contexto, el manejo adecuado de excepciones se convierte en una disciplina fundamental.

Sin embargo, no todas las excepciones son iguales. Mientras que errores como NullPointerException o IOException señalan problemas técnicos o de infraestructura, existe una categoría más rica y expresiva: las Excepciones de Dominio. Estas no representan fallos técnicos, sino violaciones específicas de las reglas, políticas y restricciones del negocio.

Las excepciones de dominio son especialmente valiosas en arquitecturas que siguen los principios de Domain-Driven Design (DDD), ya que permiten que nuestro código comunique directamente en el lenguaje del negocio, encapsulando la lógica de forma explícita y comprensible.

Este artículo establece una base documental completa sobre el tema, explorando un catálogo exhaustivo de excepciones de dominio y presentando un patrón de implementación que mantiene la separación de responsabilidades entre las capas de dominio e infraestructura.

El Corazón del Asunto: Un Catálogo de Excepciones de Dominio

Una arquitectura robusta requiere identificar y nombrar los conceptos con precisión. Para los errores de negocio, esto significa crear una jerarquía de excepciones que comunique exactamente qué regla específica ha sido violada. El siguiente catálogo cubre una amplia gama de escenarios de negocio comunes:

ExcepciónDescripciónEjemplo de UsoCódigo HTTPDEFAULT_MESSAGEDEFAULT_CODE
EntityNotFoundExceptionLa entidad o recurso solicitado no existeBuscar un usuario con id=99 que no está en la base de datos404 Not FoundENTITY_NOT_FOUND404-001
DuplicateEntityExceptionYa existe una entidad con un identificador únicoCrear un usuario con un email ya registrado409 ConflictDUPLICATE_ENTITY409-001
InvalidIdentifierExceptionEl identificador no cumple con el formato requeridoConsultar un producto con ID esperado como UUID usando valor "ABC-###"400 Bad RequestINVALID_IDENTIFIER400-001
BusinessRuleViolationExceptionViolación de una regla de negocio fundamentalRetiro bancario que excede el saldo disponible422 Unprocessable EntityBUSINESS_RULE_VIOLATION422-001
OperationNotAllowedExceptionOperación no permitida en el estado actual del recursoIntentar cancelar un pedido ya entregado403 ForbiddenOPERATION_NOT_ALLOWED403-001
InconsistentStateExceptionEstado internamente incoherente en el modelo de dominioPedido marcado como “pagado” sin transacciones asociadas500 Internal Server ErrorINCONSISTENT_STATE500-001
ValidationExceptionError genérico de validación de datos de entradaPetición a la API sin campo obligatorio400 Bad RequestVALIDATION_FAILED400-002
InvalidValueExceptionValor de campo fuera de rango o inválidoCrear usuario con edad = -5400 Bad RequestINVALID_VALUE400-003
MissingMandatoryValueExceptionAusencia de valor obligatorio para la operaciónCrear factura sin número de serie400 Bad RequestMISSING_MANDATORY_VALUE400-004
ConcurrencyExceptionConflicto al modificar un recurso en paraleloDos usuarios editando el mismo producto simultáneamente409 ConflictCONCURRENCY_CONFLICT409-002
OptimisticLockingExceptionDiscordancia en versión de entidad (bloqueo optimista)Guardar cliente con version=2 cuando la BD tiene version=3409 ConflictOPTIMISTIC_LOCK_ERROR409-003
ReferentialIntegrityExceptionViolación de restricción de integridad referencialEliminar cliente que tiene facturas asociadas409 ConflictREFERENTIAL_INTEGRITY_VIOLATION409-004
AuthenticationExceptionFallo en proceso de autenticaciónIniciar sesión con contraseña incorrecta401 UnauthorizedAUTHENTICATION_FAILED401-001
AuthorizationExceptionUsuario sin permisos necesarios para la operaciónUsuario “cliente” intentando acceder a panel de administración403 ForbiddenAUTHORIZATION_FAILED403-002
SessionExpiredExceptionSesión expirada o token inválidoPetición con JWT expirado a endpoint protegido401 UnauthorizedSESSION_EXPIRED401-002
WorkflowViolationExceptionTransición inválida en flujo o procesoIntentar “aprobar” orden de compra no “validada”422 Unprocessable EntityWORKFLOW_VIOLATION422-002
TimeoutExceptionOperación excedió tiempo de espera máximoPago en pasarela externa sin respuesta a tiempo504 Gateway TimeoutOPERATION_TIMEOUT504-001
ExternalSystemUnavailableExceptionSistema externo dependiente no disponibleServicio de inventario caído durante procesamiento de venta503 Service UnavailableEXTERNAL_SYSTEM_UNAVAILABLE503-001
InsufficientBalanceExceptionFondos o saldo insuficientesPagar compra de 200€ con saldo de 100€422 Unprocessable EntityINSUFFICIENT_BALANCE422-003
CurrencyMismatchExceptionMezcla de monedas incompatiblesPagar en USD desde cuenta que opera solo en EUR400 Bad RequestCURRENCY_MISMATCH400-005
LimitExceededExceptionSuperación de límite definidoTransferir 10.000€ con límite diario de 5.000€429 Too Many RequestsLIMIT_EXCEEDED429-001
ConfigurationExceptionError o falta de configuración en el dominioSistema sin tipo de IVA definido para país específico500 Internal Server ErrorCONFIGURATION_ERROR500-002
UnsupportedOperationExceptionOperación no soportada o implementadaExportar reporte a formato obsoleto no desarrollado501 Not ImplementedUNSUPPORTED_OPERATION501-001

Patrón de Implementación: De la Pureza del Dominio a la Realidad de la Infraestructura 💡

Tener una rica jerarquía de excepciones es valioso, pero el verdadero desafío está en manejarlas de forma elegante. El objetivo es que nuestra capa de dominio lance una InsufficientBalanceException sin conocimiento alguno sobre HTTP, mientras que nuestra capa de API REST la traduzca apropiadamente a una respuesta 422 Unprocessable Entity con formato JSON.

La solución combina el Principio de Inversión de Dependencias con el patrón Strategy, creando un sistema flexible y escalable.

1. La Base de Todo: DomainException

Creamos una clase base abstracta de la que heredarán todas nuestras excepciones de dominio. Es un POJO puro, sin dependencias de frameworks:

package com.tuempresa.dominio.excepciones;

/**
 * Excepción base del dominio.
 * 
 * Todas las excepciones específicas del dominio deben heredar de esta clase.
 * No contiene ninguna referencia a frameworks ni tecnologías (HTTP, DB, etc.)
 * 
 * Permite mantener un "errorCode" que facilita el mapeo en las capas de
 * aplicación/infraestructura.
 */
public abstract class DomainException extends RuntimeException {
    
    private final String errorCode;
    
    protected DomainException(String message, String errorCode) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public String getErrorCode() {
        return errorCode;
    }
}

2. Una Excepción Concreta: EntityNotFoundException

Cada excepción de dominio es una clase simple que extiende DomainException, proporcionando sus propios códigos y mensajes por defecto:

package com.tuempresa.dominio.excepciones;

/**
 * Se lanza cuando una entidad no puede ser encontrada en el dominio.
 */
public class EntityNotFoundException extends DomainException {
    
    private static final String DEFAULT_MESSAGE = "ENTITY_NOT_FOUND";
    private static final String DEFAULT_CODE = "404-001";
    
    public EntityNotFoundException() {
        super(DEFAULT_MESSAGE, DEFAULT_CODE);
    }
    
    // Constructor opcional para mayor flexibilidad
    public EntityNotFoundException(String message, String errorCode) {
        super(message, errorCode);
    }
}

3. El Traductor: Patrón Strategy para el Manejo de Excepciones

En lugar de un gigantesco bloque if-else o switch, creamos una “estrategia” de manejo para cada excepción. Esto respeta el Principio de Abierto/Cerrado: podemos añadir nuevos manejadores sin modificar código existente.

3.1. La Interfaz Común (DomainExceptionHandlerStrategy)

Define el contrato que todos nuestros manejadores deben cumplir:

package com.tuempresa.infraestructura.excepciones;

import com.tuempresa.dominio.excepciones.DomainException;
import org.springframework.http.ResponseEntity;

public interface DomainExceptionHandlerStrategy<T extends DomainException> {
    
    /**
     * Devuelve el tipo de excepción que este manejador puede procesar.
     */
    Class<T> getExceptionType();
    
    /**
     * Procesa la excepción y la convierte en una respuesta HTTP.
     */
    ResponseEntity<ApiError> handle(T ex);
}

3.2. Un Manejador Específico (EntityNotFoundHandler)

Implementación concreta para EntityNotFoundException. Su única responsabilidad es traducir esta excepción de dominio en un HTTP 404 Not Found:

package com.tuempresa.infraestructura.excepciones;

import com.tuempresa.dominio.excepciones.EntityNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;

@Component
public class EntityNotFoundHandler 
    implements DomainExceptionHandlerStrategy<EntityNotFoundException> {
    
    @Override
    public Class<EntityNotFoundException> getExceptionType() {
        return EntityNotFoundException.class;
    }
    
    @Override
    public ResponseEntity<ApiError> handle(EntityNotFoundException ex) {
        ApiError error = new ApiError(ex.getErrorCode(), ex.getMessage());
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }
}

4. El Orquestador: DomainExceptionHandlerRegistry 🔨

Este componente central actúa como director de orquesta. Mediante inyección de dependencias de Spring, recibe un Map donde las claves son tipos de excepción y los valores son las estrategias correspondientes:

package com.tuempresa.infraestructura.excepciones;

import com.tuempresa.dominio.excepciones.DomainException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

@Component
public class DomainExceptionHandlerRegistry {
    
    private final Map<Class<? extends DomainException>, DomainExceptionHandlerStrategy> strategies;
    
    // Spring inyectará una lista de todos los beans que implementen la interfaz
    // y nosotros la convertimos en un Map para un acceso rápido.
    public DomainExceptionHandlerRegistry(
        java.util.List<DomainExceptionHandlerStrategy> strategyList) {
        
        this.strategies = strategyList.stream()
            .collect(Collectors.toMap(
                DomainExceptionHandlerStrategy::getExceptionType, 
                Function.identity()
            ));
    }
    
    @SuppressWarnings("unchecked")
    public ResponseEntity<ApiError> handle(DomainException ex) {
        // Buscamos la estrategia específica para el tipo de excepción
        DomainExceptionHandlerStrategy<DomainException> strategy = 
            strategies.get(ex.getClass());
        
        if (strategy != null) {
            return strategy.handle(ex);
        }
        
        // Fallback para excepciones de dominio no mapeadas explícitamente
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ApiError("500-000", "UNEXPECTED_DOMAIN_ERROR"));
    }
}

5. La Estructura de Respuesta: ApiError DTO

Un record de Java para estandarizar el formato de nuestras respuestas de error:

package com.tuempresa.infraestructura.excepciones;

// Usamos un record de Java para una clase de datos inmutable y concisa.
public record ApiError(String code, String message) {}

6. La Puerta de Entrada: @RestControllerAdvice

Finalmente, usamos @RestControllerAdvice de Spring para crear un traductor global que intercepta cualquier DomainException no capturada anteriormente:

package com.tuempresa.infraestructura.excepciones;

import com.tuempresa.dominio.excepciones.DomainException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@RestControllerAdvice
public class GlobalExceptionTranslator extends ResponseEntityExceptionHandler {
    
    private final DomainExceptionHandlerRegistry registry;
    
    public GlobalExceptionTranslator(DomainExceptionHandlerRegistry registry) {
        this.registry = registry;
    }
    
    @ExceptionHandler(DomainException.class)
    public final ResponseEntity<ApiError> handleDomainException(DomainException ex) {
        // Toda la lógica compleja está en el registry, aquí solo delegamos.
        return registry.handle(ex);
    }
}

Beneficios del Patrón Implementado

Este enfoque proporciona múltiples ventajas significativas:

Separación de Responsabilidades: La capa de dominio permanece completamente aislada de las preocupaciones de infraestructura como códigos HTTP o formatos de respuesta.

Extensibilidad: Añadir nuevas excepciones de dominio requiere únicamente crear la excepción y su manejador correspondiente, sin modificar código existente.

Testabilidad: Cada componente puede ser probado independientemente, facilitando la escritura de pruebas unitarias y de integración.

Mantenibilidad: La lógica de manejo de errores está centralizada pero distribuida de forma lógica, evitando el antipatrón de “God Objects”.

Reutilización: El mismo patrón puede adaptarse a diferentes protocolos y tecnologías más allá de HTTP/REST.

Conclusión y Futuras Líneas de Trabajo

Hemos establecido una base sólida y documentada para el manejo de errores de negocio. Las excepciones de dominio trascienden la simple gestión de errores para convertirse en una herramienta de modelado que enriquece nuestro código, haciéndolo más expresivo y alineado con las reglas del negocio.

El patrón presentado, fundamentado en Strategy y un Registro central, ofrece una solución elegante que mantiene la pureza de la capa de dominio mientras proporciona un mecanismo extensible para traducir errores de negocio en respuestas concretas de infraestructura.

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
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
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
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
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
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