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

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

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

Transacciones Declarativas en Arquitecturas Hexagonales con Spring WebFlux y AOP

Transacciones Declarativas en Arquitecturas Hexagonales con Spring WebFlux y AOP

En el desarrollo de aplicaciones reactivas modernas, especialmente bajo paradigmas como la Arquitectura Hexagonal, surgen desafíos que nos obligan a repensar cómo aplicamos conceptos transversales. Un

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

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

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

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

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

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

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

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

Descubre el Poder del SemVer: Optimiza el Versionado de tu Software y Mantén un CHANGELOG Excepcional

Descubre el Poder del SemVer: Optimiza el Versionado de tu Software y Mantén un CHANGELOG Excepcional

El Versionado Semántico (SemVer) es una herramienta fundamental para comunicar de forma precisa los cambios en el software, facilitando el mantenimiento y la colaboración. Complementarlo con un **

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

Asegurando el Tejido Distribuido: Una Guía para la Seguridad en Arquitecturas de Microservicios

Asegurando el Tejido Distribuido: Una Guía para la Seguridad en Arquitecturas de Microservicios

El viaje hacia arquitecturas de microservicios ha transformado la forma en que construimos y desplegamos software. La agilidad, escalabilidad y resiliencia que ofrecen son innegables. Sin embargo, est

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

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

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

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

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

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

Observabilidad sin Ruido: Diseñando un Sistema de Logs con AOP en Arquitecturas DDD

Observabilidad sin Ruido: Diseñando un Sistema de Logs con AOP en Arquitecturas DDD

Hay una tensión que todo equipo de desarrollo enfrenta tarde o temprano: la necesidad de saber qué está pasando dentro del sistema sin que esa necesidad contamine el código que lo hace funcionar. Los

Arquitectura Modular por Contexto: Cuando la Teoría se Encuentra con la Realidad

Arquitectura Modular por Contexto: Cuando la Teoría se Encuentra con la Realidad

Has estado ahí. Es lunes por la mañana, abres el proyecto en tu IDE, y necesitas modificar cómo se procesa un pedido. Treinta minutos después, todavía estás navegando entre carpetas intentando encontr

Observabilidad sin Ruido: Diseñando un Sistema de Logs con AOP en Arquitecturas DDD — Parte II

Observabilidad sin Ruido: Diseñando un Sistema de Logs con AOP en Arquitecturas DDD — Parte II

La primera parte de este artículo construyó el argumento conceptual: por qué los logs dispersos se convierten en deuda técnica, cómo AOP permite centralizar la observabilidad sin contaminar la lógica

Observabilidad sin Ruido: Diseñando un Sistema de Logs con AOP en Arquitecturas DDD — Parte III

Observabilidad sin Ruido: Diseñando un Sistema de Logs con AOP en Arquitecturas DDD — Parte III

Las dos primeras partes de esta serie resolvieron un problema bien delimitado: construir un sistema de logging centralizado que operara de forma transversal sobre una arquitectura DDD sin contaminar l

El tramo en cascada que todo proyecto ágil necesita (y casi ninguno tiene)

El tramo en cascada que todo proyecto ágil necesita (y casi ninguno tiene)

Llevas un tiempo trabajando en equipos que dicen practicar Scrum, y algo no termina de cuadrarte. Los sprints avanzan, la demo sale en tiempo, el tablero se vacía cada dos semanas. Y aun así, al cabo