Tags (73)
- Agile
- Alta disponibilidad
- Alternativas cloud
- Aop
- Arquitectura
- Arquitectura distribuida
- Automatizacion
- Aws
- Azure devops
- Base de datos
- Buenas practicas
- Cloud
- Colas
- Competing consumers
- Convenciones
- Copilot
- Diseno
- Docker
- Docker compose
- Documentacion
- Eda
- Equipos
- Escalabilidad
- Flujo de negocio
- Flujo de trabajo
- Flyway
- Git
- Gradle
- Herramientas digitales
- Ia
- Iam
- Infraestructura
- Java
- Jerarquia tecnica
- Jpa
- Jsonb
- Kafka
- Kubernetes
- Liderazgo en software
- Lineamientos
- Log
- Logging
- Microservicios
- Mongodb
- Monitoreo
- Nosql
- Observabilidad
- Open source
- Plugins
- Postgresql
- Privacidad
- Programacion funcional
- Programacion reactiva
- Rabbitmq
- Rotacion de talento
- Saga
- Scrum
- Security
- Seguridad
- Self hosting
- Sistemas legados
- Snippets
- Spring boot
- Spring mvc
- Sql
- Streams
- Threadlocal
- Trazabilidad
- Versionado
- Web
- Webflux
- Websockets
- Zero trust
Manejando el Caos: Una Guía Definitiva sobre Excepciones de Dominio 🎯
- Mauricio ECR
- Snippets
- 20 Aug, 2025
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
Manejando el Caos: Una Guía Definitiva sobre Excepciones de Dominio 🎯
- Mauricio ECR
- Snippets
- 20 Aug, 2025
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ón | Descripción | Ejemplo de Uso | Código HTTP | DEFAULT_MESSAGE | DEFAULT_CODE |
|---|---|---|---|---|---|
| EntityNotFoundException | La entidad o recurso solicitado no existe | Buscar un usuario con id=99 que no está en la base de datos |
404 Not Found | ENTITY_NOT_FOUND |
404-001 |
| DuplicateEntityException | Ya existe una entidad con un identificador único | Crear un usuario con un email ya registrado | 409 Conflict | DUPLICATE_ENTITY |
409-001 |
| InvalidIdentifierException | El identificador no cumple con el formato requerido | Consultar un producto con ID esperado como UUID usando valor "ABC-###" |
400 Bad Request | INVALID_IDENTIFIER |
400-001 |
| BusinessRuleViolationException | Violación de una regla de negocio fundamental | Retiro bancario que excede el saldo disponible | 422 Unprocessable Entity | BUSINESS_RULE_VIOLATION |
422-001 |
| OperationNotAllowedException | Operación no permitida en el estado actual del recurso | Intentar cancelar un pedido ya entregado | 403 Forbidden | OPERATION_NOT_ALLOWED |
403-001 |
| InconsistentStateException | Estado internamente incoherente en el modelo de dominio | Pedido marcado como "pagado" sin transacciones asociadas | 500 Internal Server Error | INCONSISTENT_STATE |
500-001 |
| ValidationException | Error genérico de validación de datos de entrada | Petición a la API sin campo obligatorio | 400 Bad Request | VALIDATION_FAILED |
400-002 |
| InvalidValueException | Valor de campo fuera de rango o inválido | Crear usuario con edad = -5 |
400 Bad Request | INVALID_VALUE |
400-003 |
| MissingMandatoryValueException | Ausencia de valor obligatorio para la operación | Crear factura sin número de serie | 400 Bad Request | MISSING_MANDATORY_VALUE |
400-004 |
| ConcurrencyException | Conflicto al modificar un recurso en paralelo | Dos usuarios editando el mismo producto simultáneamente | 409 Conflict | CONCURRENCY_CONFLICT |
409-002 |
| OptimisticLockingException | Discordancia en versión de entidad (bloqueo optimista) | Guardar cliente con version=2 cuando la BD tiene version=3 |
409 Conflict | OPTIMISTIC_LOCK_ERROR |
409-003 |
| ReferentialIntegrityException | Violación de restricción de integridad referencial | Eliminar cliente que tiene facturas asociadas | 409 Conflict | REFERENTIAL_INTEGRITY_VIOLATION |
409-004 |
| AuthenticationException | Fallo en proceso de autenticación | Iniciar sesión con contraseña incorrecta | 401 Unauthorized | AUTHENTICATION_FAILED |
401-001 |
| AuthorizationException | Usuario sin permisos necesarios para la operación | Usuario "cliente" intentando acceder a panel de administración | 403 Forbidden | AUTHORIZATION_FAILED |
403-002 |
| SessionExpiredException | Sesión expirada o token inválido | Petición con JWT expirado a endpoint protegido | 401 Unauthorized | SESSION_EXPIRED |
401-002 |
| WorkflowViolationException | Transición inválida en flujo o proceso | Intentar "aprobar" orden de compra no "validada" | 422 Unprocessable Entity | WORKFLOW_VIOLATION |
422-002 |
| TimeoutException | Operación excedió tiempo de espera máximo | Pago en pasarela externa sin respuesta a tiempo | 504 Gateway Timeout | OPERATION_TIMEOUT |
504-001 |
| ExternalSystemUnavailableException | Sistema externo dependiente no disponible | Servicio de inventario caído durante procesamiento de venta | 503 Service Unavailable | EXTERNAL_SYSTEM_UNAVAILABLE |
503-001 |
| InsufficientBalanceException | Fondos o saldo insuficientes | Pagar compra de 200€ con saldo de 100€ | 422 Unprocessable Entity | INSUFFICIENT_BALANCE |
422-003 |
| CurrencyMismatchException | Mezcla de monedas incompatibles | Pagar en USD desde cuenta que opera solo en EUR | 400 Bad Request | CURRENCY_MISMATCH |
400-005 |
| LimitExceededException | Superación de límite definido | Transferir 10.000€ con límite diario de 5.000€ | 429 Too Many Requests | LIMIT_EXCEEDED |
429-001 |
| ConfigurationException | Error o falta de configuración en el dominio | Sistema sin tipo de IVA definido para país específico | 500 Internal Server Error | CONFIGURATION_ERROR |
500-002 |
| UnsupportedOperationException | Operación no soportada o implementada | Exportar reporte a formato obsoleto no desarrollado | 501 Not Implemented | UNSUPPORTED_OPERATION |
501-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.
Una Propuesta para Estandarizar la Seguridad en APIs REST con Arquitectura Hexagonal y Spring Security
- Mauricio ECR
- Snippets
- 03 Aug, 2025
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
Una Propuesta para Estandarizar la Seguridad en APIs REST con Arquitectura Hexagonal y Spring Security
- Mauricio ECR
- Snippets
- 03 Aug, 2025
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 tiempo, compatible con los estándares de la industria (como JWT y Spring Security) puede ser un reto. Este artículo explora una solución basada en la arquitectura hexagonal, que permite centralizar la lógica de autorización en el dominio, sin perder la integración con las capacidades avanzadas de Spring Security, como el uso de anotaciones (@PreAuthorize) y la inyección del usuario autenticado en los controladores.
Contexto y Desafío
La mayoría de los frameworks modernos, como Spring Boot, ofrecen mecanismos de seguridad robustos y listos para usar. Sin embargo, estos suelen acoplar la lógica de autenticación y autorización a la infraestructura, dificultando la reutilización y el testeo independiente del dominio. Por otro lado, la arquitectura hexagonal promueve la separación de responsabilidades, permitiendo que la lógica de negocio (incluida la autorización) permanezca independiente de los detalles tecnológicos.
El desafío surge cuando se requiere que la lógica de autorización, implementada en el dominio, pueda interactuar con el ecosistema de Spring Security, permitiendo el uso de anotaciones como @PreAuthorize y la inyección del usuario autenticado en los controladores. La solución propuesta en este artículo aborda este reto, permitiendo una integración fluida y flexible.
Dependencias Requeridas
Para implementar esta solución, se requieren las siguientes dependencias en el archivo build.gradle:
dependencies {
// Spring Boot Core
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-configuration-processor'
// Lombok para reducir boilerplate
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// JWT (opcional, para parsing de tokens)
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
// Testing
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
Estructura de Directorios
La librería sigue una estructura de directorios que respeta los principios de la arquitectura hexagonal:
src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── tuempresa/
│ │ ├── dominio/
│ │ │ └── autorizacion/
│ │ │ ├── AuthorizationContext.java
│ │ │ ├── AuthorizationException.java
│ │ │ ├── AuthorizationService.java
│ │ │ └── AuthorizationServiceImpl.java
│ │ └── infraestructura/
│ │ ├── config/
│ │ │ ├── AuthorizationConfig.java
│ │ │ └── AuthorizationFilterConfig.java
│ │ └── filtros/
│ │ └── StandardAuthorizationFilter.java
│ └── resources/
│ └── META-INF/
│ └── spring.factories (para auto-configuración)
└── test/
└── java/
└── com/
└── tuempresa/
├── dominio/
│ └── autorizacion/
│ └── AuthorizationServiceTest.java
└── infraestructura/
└── filtros/
└── StandardAuthorizationFilterTest.java
Solución: Librería de Seguridad Hexagonal Integrada
La solución se basa en una serie de clases y componentes que pueden ser empaquetados como una librería reutilizable. Esta librería permite:
- Centralizar la lógica de autorización en el dominio, desacoplada de la infraestructura.
- Configurar rutas excluidas, parámetros y comportamiento desde archivos de configuración.
- Integrar con Spring Security para habilitar anotaciones y acceso al usuario autenticado.
- Validar JWT y extraer roles/claims para el contexto de seguridad.
A continuación, se presentan las clases clave de la solución.
1. Contexto de Autorización (Dominio)
package com.tuempresa.dominio.autorizacion;
import lombok.Builder;
import lombok.Value;
import java.util.Map;
@Value
@Builder
public class AuthorizationContext {
String method;
String uri;
Map<String, String> headers;
Map<String, String> queryParams;
String body;
String remoteAddress;
}
2. Excepción de Dominio
package com.tuempresa.dominio.autorizacion;
public class AuthorizationException extends RuntimeException {
public AuthorizationException(String message) {
super(message);
}
}
3. Puerto de Dominio (Interface)
package com.tuempresa.dominio.autorizacion;
public interface AuthorizationService {
void authorize(AuthorizationContext context) throws AuthorizationException;
}
4. Implementación Base del Servicio de Dominio
package com.tuempresa.dominio.autorizacion;
public class AuthorizationServiceImpl implements AuthorizationService {
@Override
public void authorize(AuthorizationContext context) {
String token = context.getHeaders().get("authorization");
if (token == null || !token.startsWith("Bearer ")) {
throw new AuthorizationException("Token inválido o ausente");
}
// Más lógica de dominio...
}
}
5. Configuración del Filtro
package com.tuempresa.infraestructura.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Set;
import java.util.HashSet;
@Data
@Component
@ConfigurationProperties(prefix = "app.security.authorization")
public class AuthorizationFilterConfig {
private Set<String> excludedPaths = new HashSet<>();
private boolean enabled = true;
private int order = 1;
private boolean includeQueryParams = true;
private boolean includeBody = false;
private boolean enableSpringSecurityIntegration = true;
public AuthorizationFilterConfig() {
excludedPaths.add("/actuator/health");
excludedPaths.add("/actuator/info");
excludedPaths.add("/swagger-ui");
excludedPaths.add("/v3/api-docs");
excludedPaths.add("/error");
}
}
6. Filtro Principal Consolidado
package com.tuempresa.infraestructura.filtros;
import com.tuempresa.dominio.autorizacion.AuthorizationContext;
import com.tuempresa.dominio.autorizacion.AuthorizationService;
import com.tuempresa.dominio.autorizacion.AuthorizationException;
import com.tuempresa.infraestructura.config.AuthorizationFilterConfig;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
@Component
@Order(1)
@ConditionalOnProperty(name = "app.security.authorization.enabled", havingValue = "true", matchIfMissing = true)
@RequiredArgsConstructor
public class StandardAuthorizationFilter extends OncePerRequestFilter {
private final AuthorizationFilterConfig config;
private final AuthorizationService authorizationService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (isExcludedPath(request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}
try {
HttpServletRequest requestToUse = request;
if (config.isIncludeBody()) {
requestToUse = new ContentCachingRequestWrapper(request);
}
Map<String, String> headers = extractHeaders(requestToUse);
Map<String, String> queryParams = config.isIncludeQueryParams() ? extractQueryParams(requestToUse) : Collections.emptyMap();
String requestBody = (config.isIncludeBody() && requestToUse instanceof ContentCachingRequestWrapper)
? extractRequestBody((ContentCachingRequestWrapper) requestToUse)
: null;
AuthorizationContext context = AuthorizationContext.builder()
.method(requestToUse.getMethod())
.uri(requestToUse.getRequestURI())
.headers(headers)
.queryParams(queryParams)
.body(requestBody)
.remoteAddress(requestToUse.getRemoteAddr())
.build();
authorizationService.authorize(context);
// Integración con Spring Security (si está habilitada)
if (config.isEnableSpringSecurityIntegration()) {
setSpringSecurityContext(context);
}
filterChain.doFilter(requestToUse, response);
} catch (AuthorizationException e) {
handleSecurityException(response, e);
} catch (Exception e) {
logger.error("Error in authorization filter", e);
handleGenericError(response);
}
}
private Map<String, String> extractHeaders(HttpServletRequest request) {
Map<String, String> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
headers.put(headerName.toLowerCase(), headerValue);
}
return headers;
}
private Map<String, String> extractQueryParams(HttpServletRequest request) {
Map<String, String> queryParams = new HashMap<>();
Enumeration<String> paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
String paramValue = request.getParameter(paramName);
queryParams.put(paramName, paramValue);
}
return queryParams;
}
private String extractRequestBody(ContentCachingRequestWrapper request) throws IOException {
byte[] content = request.getContentAsByteArray();
if (content.length > 0) {
return new String(content, StandardCharsets.UTF_8);
}
return null;
}
private void setSpringSecurityContext(AuthorizationContext context) {
try {
String authHeader = context.getHeaders().get("authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String username = extractUsernameFromToken(authHeader);
List<String> roles = extractRolesFromToken(authHeader);
List<SimpleGrantedAuthority> authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
.toList();
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.warn("Could not set Spring Security context", e);
}
}
private String extractUsernameFromToken(String authHeader) {
// Implementar parsing del JWT aquí
return "user_from_jwt"; // Placeholder
}
private List<String> extractRolesFromToken(String authHeader) {
// Implementar parsing del JWT aquí
return List.of("USER"); // Placeholder
}
private boolean isExcludedPath(String requestURI) {
return config.getExcludedPaths().stream()
.anyMatch(excluded -> requestURI.startsWith(excluded));
}
private void handleSecurityException(HttpServletResponse response, AuthorizationException e) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write(String.format(
"{\"error\":\"Unauthorized\",\"message\":\"%s\",\"timestamp\":\"%s\"}",
e.getMessage(), new Date()
));
}
private void handleGenericError(HttpServletResponse response) throws IOException {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType("application/json");
response.getWriter().write(String.format(
"{\"error\":\"Internal Server Error\",\"message\":\"Authorization check failed\",\"timestamp\":\"%s\"}",
new Date()
));
}
}
7. Configuración de Beans
package com.tuempresa.infraestructura.config;
import com.tuempresa.dominio.autorizacion.AuthorizationService;
import com.tuempresa.dominio.autorizacion.AuthorizationServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AuthorizationConfig {
@Bean
public AuthorizationService authorizationService() {
return new AuthorizationServiceImpl();
}
}
8. Configuración en application.yml
app:
security:
authorization:
enabled: true
order: 1
include-query-params: true
include-body: false
enable-spring-security-integration: true
excluded-paths:
- "/actuator/health"
- "/actuator/info"
- "/swagger-ui"
- "/v3/api-docs"
- "/public"
- "/auth/login"
9. Ejemplo de Uso en el Proyecto Cliente
@Service
public class MyCustomAuthorizationService implements AuthorizationService {
@Override
public void authorize(AuthorizationContext context) throws AuthorizationException {
String token = context.getHeaders().get("authorization");
if (!isValidJWT(token)) {
throw new AuthorizationException("JWT inválido");
}
if (context.getUri().startsWith("/admin") && !hasAdminRole(token)) {
throw new AuthorizationException("Acceso denegado a área administrativa");
}
}
private boolean isValidJWT(String token) {
// Implementar validación JWT
return true;
}
private boolean hasAdminRole(String token) {
// Verificar roles en el JWT
return false;
}
}
10. Uso en Controllers
@RestController
public class MyController {
@GetMapping("/protected")
@PreAuthorize("hasRole('USER')")
public String protectedEndpoint(@AuthenticationPrincipal String username) {
return "Hello " + username + "! You are authenticated.";
}
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminEndpoint() {
return "Admin area";
}
}
Consideraciones Finales
- Separación de responsabilidades: El dominio se mantiene puro y desacoplado de la infraestructura.
- Integración total: Se habilita el uso de anotaciones y la inyección del usuario autenticado gracias a la integración con el contexto de Spring Security.
- Configurabilidad: La solución es fácilmente adaptable a distintos proyectos mediante configuración externa.
- Reutilización: El diseño modular permite empaquetar la solución como una librería para múltiples aplicaciones.
Conclusión
La estandarización de la seguridad bajo una arquitectura hexagonal, combinada con la integración de Spring Security y JWT, permite construir aplicaciones robustas, mantenibles y alineadas con las mejores prácticas de la industria. Esta aproximación no solo facilita la reutilización y el testeo, sino que también habilita la evolución futura del sistema, permitiendo incorporar nuevas estrategias de autenticación o autorización sin comprometer la arquitectura.
Tablas Normalizadas vs. JSON/JSONB en PostgreSQL
- Mauricio ECR
- Persistencia
- 11 Jul, 2025
En el diseño de bases de datos, la normalización ha sido durante mucho tiempo sinónimo de integridad, eficiencia y orden. Sin embargo, los tiempos cambian, y con ellos, las necesidades de los sistemas
Tablas Normalizadas vs. JSON/JSONB en PostgreSQL
- Mauricio ECR
- Persistencia
- 11 Jul, 2025
En el diseño de bases de datos, la normalización ha sido durante mucho tiempo sinónimo de integridad, eficiencia y orden. Sin embargo, los tiempos cambian, y con ellos, las necesidades de los sistemas modernos. Los datos semi-estructurados ganan terreno, y PostgreSQL ha sabido adaptarse integrando soporte robusto para los tipos JSON y JSONB. Esta evolución plantea una pregunta crucial: ¿seguir apostando por la rigidez de las tablas normalizadas o abrazar la elasticidad del modelo documental?
El Dilema: Estructura vs. Flexibilidad
La decisión entre un modelo relacional rígido y uno dinámico basado en documentos tiene implicaciones profundas en rendimiento, mantenibilidad y escalabilidad. Entender sus ventajas y límites es clave para construir sistemas sólidos y adaptables.
Tablas Normalizadas: Precisión con Disciplina
La normalización organiza datos para evitar duplicidades y asegurar integridad, a través de estructuras bien definidas y relaciones explícitas.
Ventajas:
- Integridad de Datos: Claves foráneas, restricciones
UNIQUEy validacionesCHECKaseguran coherencia. - Eficiencia en Escrituras: Modificaciones atómicas reducen el riesgo de anomalías.
- Ahorro de Espacio: La minimización de redundancia optimiza el almacenamiento.
- Consultas Optimizadas: Los
JOINson eficientemente resueltos por el planificador de PostgreSQL.
Desventajas:
- Cambios Costosos: Alterar la estructura requiere migraciones.
- Complejidad en Consultas: Obtener una visión completa puede implicar múltiples
JOIN. - Lecturas Pesadas: Agregaciones sobre muchas tablas pueden degradar el rendimiento.
Cuándo Usarlas: Cuando los datos tienen una estructura estable y la integridad es prioritaria. Casos típicos incluyen sistemas contables, gestión de inventarios y aplicaciones bancarias.
JSON/JSONB: Flexibilidad sin Esquema
PostgreSQL permite almacenar JSON de dos maneras:
json: Mantiene el texto original. Más rápido al insertar, pero más lento en consultas.jsonb: Almacena en formato binario. Un poco más lento al insertar, pero mucho más eficiente al consultar y permite indexación avanzada. En la mayoría de los casos, es la opción recomendada.
Ventajas:
- Esquema Dinámico: Atributos variables sin necesidad de alterar el modelo.
- Consultas Directas: Datos relacionados pueden vivir en un único documento.
- Prototipado Rápido: Ideal para iterar sin fricciones durante el desarrollo.
Desventajas:
- Sin Integridad Referencial: Las relaciones deben ser gestionadas manualmente.
- Redundancia y Consistencia: Datos duplicados son comunes, lo que implica riesgos si no se sincronizan.
- Actualizaciones Complejas: Modificar datos anidados no es tan directo como un
UPDATE.
Cuando destaca: Para casos con estructuras cambiantes, como configuraciones, eventos, integración de APIs externas o metadata variable
JSONB: Consultas, Índices y Más
Consultas y Proyecciones
PostgreSQL ofrece operadores intuitivos para navegar por estructuras JSONB:
->: Accede a un campo, devuelvejsonb.->>: Accede y devuelve texto.#>: Navega rutas anidadas, devuelvejsonb.#>>: Igual que#>, pero como texto.
Ejemplo de Uso:
CREATE TABLE productos (
id SERIAL PRIMARY KEY,
nombre TEXT NOT NULL,
detalles JSONB
);
INSERT INTO productos (nombre, detalles) VALUES
('Laptop Pro', '{"precio": 1500, "fabricante": "TechCorp", "especs": {"cpu": "i7", "ram": 16, "almacenamiento": 512}}'),
('Smartphone X', '{"precio": 800, "fabricante": "MobileFirst", "especs": {"cpu": "Snapdragon 8", "ram": 8, "almacenamiento": 256}}');
Proyecciones:
SELECT nombre, detalles->>'precio' AS precio FROM productos;
SELECT nombre, detalles#>'{especs, ram}' AS ram FROM productos;
Filtrado y Búsquedas
Operadores potentes permiten extraer información fácilmente:
@>: Contiene.<@: Está contenido.?: Existe clave.?|: Existe alguna.?&: Existen todas.
Ejemplos:
SELECT * FROM productos WHERE detalles @> '{"fabricante": "TechCorp"}';
SELECT * FROM productos WHERE detalles @> '{"especs": {"ram": 16}}';
SELECT * FROM productos WHERE detalles ? 'precio';
Indexación
Las consultas sobre JSONB pueden volverse lentas sin índices adecuados. PostgreSQL ofrece:
- GIN (Generalized Inverted Index): El más recomendado. Optimiza búsquedas con
@>,?,?|,?&. - GiST: Más versátil, pero menos eficiente en general.
Ejemplo:
CREATE INDEX idx_productos_detalles_gin ON productos USING GIN (detalles);
También es posible crear índices B-tree sobre campos específicos:
CREATE INDEX idx_productos_fabricante ON productos ((detalles->>'fabricante'));
Actualizaciones Parciales
Con jsonb_set, es posible modificar datos sin reescribir todo el documento:
UPDATE productos
SET detalles = jsonb_set(detalles, '{precio}', '1450')
WHERE nombre = 'Laptop Pro';
UPDATE productos
SET detalles = jsonb_set(detalles, '{especs, ram}', '32')
WHERE nombre = 'Laptop Pro';
Modelo Híbrido: Lo Mejor de Dos Mundos
Combinar estructuras relacionales con campos JSONB permite construir sistemas flexibles, sin sacrificar integridad.
Ventajas del enfoque mixto:
- Datos críticos viven en columnas estructuradas.
- Atributos variables residen en campos JSONB.
- Menos
JOINs, más velocidad. - Menos migraciones con cada cambio de requisitos.
Casos Prácticos
1. E-commerce: Productos con atributos diversos
CREATE TABLE productos (
id SERIAL PRIMARY KEY,
nombre TEXT NOT NULL,
precio DECIMAL(10, 2),
categoria_id INT REFERENCES categorias(id),
especificaciones JSONB
);
CREATE INDEX idx_especificaciones_gin ON productos USING GIN (especificaciones);
SELECT * FROM productos
WHERE categoria_id = 1
AND especificaciones @> '{"ram": "16GB", "almacenamiento": "SSD"}';
2. SaaS: Preferencias de usuario
ALTER TABLE usuarios ADD COLUMN preferencias JSONB DEFAULT '{}';
UPDATE usuarios
SET preferencias = jsonb_set(preferencias, '{tema}', '"claro"')
WHERE id = 123;
3. Logs y eventos con estructuras variables
CREATE INDEX idx_eventos_detalles_ip ON eventos ((detalles->>'ip'));
SELECT * FROM eventos
WHERE tipo = 'login'
AND detalles->>'ip' = '192.168.1.1';
Claves del Modelo Híbrido
- Desarrollo Ágil: Sin necesidad de migrar con cada cambio menor.
- Rendimiento: Índices GIN aceleran búsquedas complejas.
- Mantenibilidad: Las estructuras centrales permanecen estables.
- Integración Sencilla: Ideal para microservicios y respuestas JSON de APIs externas.
Conclusión: El Futuro es Híbrido
No se trata de elegir entre rigidez o flexibilidad, sino de combinarlas inteligentemente. PostgreSQL permite construir arquitecturas donde:
- Los datos estables viven en tablas relacionales.
- Los atributos cambiantes se encapsulan en JSONB.
- El SQL moderno los une con potencia y elegancia.
La evolución de jsonb —junto con el soporte creciente para SQL/JSON path— abre nuevas puertas. El enfoque híbrido no es una moda, es una estrategia para diseñar sistemas duraderos, escalables y listos para adaptarse a lo que viene.
🔗 Recursos Recomendados
Arquitectura DDD y Hexagonal: Construyendo Software para el Futuro
- Mauricio ECR
- Arquitectura
- 05 Jul, 2025
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
Arquitectura DDD y Hexagonal: Construyendo Software para el Futuro
- Mauricio ECR
- Arquitectura
- 05 Jul, 2025
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 en un laberinto frágil y costoso de mantener. ¿La solución? No es un framework de moda, sino una filosofía de diseño sólida. Esta guía ofrece un mapa detallado para construir software robusto, escalable y, sobre todo, alineado con el negocio, fusionando los principios del Diseño Guiado por el Dominio (DDD), la Arquitectura Limpia (Clean Architecture) y la Arquitectura Hexagonal (Puertos y Adaptadores).
Olvídate de las capas anémicas y el acoplamiento tecnológico. Aquí aprenderás a colocar el corazón de tu negocio —el dominio— en el centro del universo, protegido y aislado de los detalles mundanos de la tecnología. Prepárate para diseñar sistemas donde la lógica de negocio es la reina, la infraestructura es un sirviente intercambiable y el cambio es una oportunidad, no una amenaza.
🎯 Capa de Dominio: El Corazón del Negocio
Esta es la capa más sagrada y protegida de la arquitectura. Su único propósito es encapsular la lógica y las reglas de negocio puras, utilizando el Lenguaje Ubicuo (Ubiquitous Language) del problema que se está resolviendo. Es completamente agnóstica a la tecnología; no debe existir ninguna referencia a frameworks, bases de datos o APIs. En Arquitectura Hexagonal, esta capa es el "hexágono" central, y en Arquitectura Limpia, corresponde a los círculos internos de Entidades y Casos de Uso.
Aquí residen los componentes que modelan el negocio: Agregados, Entidades, Objetos de Valor, Eventos de Dominio, Servicios de Dominio, las interfaces de los Repositorios (que actúan como Puertos) y los Casos de Uso que orquestan toda la lógica.
| Componente | Rol y Responsabilidad Clave |
|---|---|
| Agregado (Aggregate) | Unidad de consistencia transaccional. Agrupa entidades y objetos de valor bajo una raíz (Aggregate Root) que protege las reglas de negocio del clúster. |
| Entidad (Entity) | Objeto con una identidad única que perdura en el tiempo y un ciclo de vida definido. Su identidad es lo que lo define, no sus atributos. |
| Objeto de Valor (VO) | Objeto inmutable definido por sus atributos, sin una identidad propia. Se utiliza para medir, cuantificar o describir cosas (ej. Dinero, FechaRango). |
| Evento de Dominio | Representa un suceso de negocio relevante que ya ha ocurrido. Sirve para comunicar cambios y desacoplar la lógica entre diferentes partes del sistema. |
| Servicio de Dominio | Encapsula lógica de negocio sin estado que no pertenece de forma natural a ninguna entidad u objeto de valor, a menudo coordinando varios de ellos. |
| Repositorio (Puerto) | Define el contrato para persistir y recuperar agregados. Es una interfaz que dicta las necesidades del dominio sin conocer la tecnología subyacente. |
| Caso de Uso | Orquesta el flujo de una operación. Es el punto de entrada a la lógica de dominio, recibiendo datos de entrada y utilizando los puertos para ejecutar la acción. |
🔌 Capa de Infraestructura: El Mundo de la Tecnología
Esta capa contiene todos los detalles técnicos y las implementaciones concretas. Su finalidad es servir como un conjunto de adaptadores que traducen las interacciones del mundo exterior al lenguaje del dominio, y viceversa. Implementa los puertos definidos en la Capa de Dominio, cumpliendo con la sagrada Regla de la Dependencia: la infraestructura siempre depende del dominio. Aquí residen los frameworks web, las conexiones a bases de datos, los clientes de servicios externos y cualquier otra dependencia del mundo real.
Los componentes clave son los Entry-Points (adaptadores que invocan los casos de uso, como controladores de API REST) y los Driven-Adapters (implementaciones de los puertos del dominio, como un repositorio JPA), junto con sus artefactos de apoyo como DTOs, Modelos de Persistencia y Mappers.
| Componente | Rol y Responsabilidad Clave |
|---|---|
| Entry-Point | Adaptador que recibe una señal externa (ej. una petición HTTP, un mensaje de una cola) y la traduce en una llamada a un Caso de Uso. |
| DTO (Data Transfer Object) | Define la estructura de datos para la comunicación externa. Se usa en los Entry-Points para modelar peticiones y respuestas, aislando el dominio. |
| Driven-Adapter | Implementación de un puerto del dominio. Por ejemplo, un repositorio que usa JPA para hablar con una base de datos o un cliente HTTP para consumir otra API. |
| Modelo de Persistencia | Clase que mapea a una estructura de base de datos (ej. una tabla). Es un detalle de implementación del adaptador de persistencia, no es la entidad de dominio. |
| Mapper / Traductor | Utilidad para convertir datos entre capas: DTO ↔ Entidad, Modelo de Persistencia ↔ Entidad. Es el pegamento que permite el desacoplamiento. |
🚀 Capa de Aplicación: El Ensamblador
Esta es la capa más externa y conceptualmente simple. Su única finalidad es ensamblar la aplicación y ponerla en marcha. No contiene lógica de negocio. Es responsable de inicializar el sistema, configurar el contenedor de Inyección de Dependencias (IoC) para conectar las implementaciones de la infraestructura (Driven-Adapters) con las abstracciones del dominio (Puertos), y leer configuraciones externas.
| Componente | Rol y Responsabilidad Clave |
|---|---|
| Contenedor IoC | Configuración de la Inyección de Dependencias. Define "recetas" para construir los objetos, especificando qué Driven-Adapter se debe usar para un Puerto. |
| Configuración | Gestiona los parámetros externos de la aplicación (URLs, credenciales, etc.) a través de archivos (.yml, .properties) o variables de entorno. |
| Punto de Entrada | La clase que contiene el método public static void main(String[] args). Su única función es arrancar el framework y, con él, toda la aplicación. |
📂 Estructura de Módulos Sugerida
Una representación visual de una estructura de módulos (por ejemplo, en Gradle o Maven) que materializa esta arquitectura de forma limpia.
mi-proyecto-escalable/
├── build.gradle.kts
├── settings.gradle.kts # Define los módulos del proyecto
│
├── applications/
│ └── app-service/ # Capa de Aplicación: Ensambla y corre la app
│ └── src/main/java/com/miempresa/app/MainApplication.java
│ └── build.gradle.kts # Depende de 'domain' e 'infrastructure'
│
├── domain/ # Capa de Dominio: Lógica de negocio pura
│ ├── model/ # El modelo: agregados, entidades, VOs, eventos...
│ │ └── src/main/java/com/miempresa/domain/model/producto/Producto.java
│ │ └── src/main/java/com/miempresa/domain/model/producto/gateways/ProductoRepository.java # Puerto (Interfaz)
│ │ └── build.gradle.kts # No tiene dependencias de otras capas
│ └── usecase/ # Los casos de uso que orquestan el modelo
│ └── src/main/java/com/miempresa/domain/usecase/producto/ListarProductosUseCase.java
│ └── build.gradle.kts # Depende de 'domain/model'
│
└── infrastructure/ # Capa de Infraestructura: Detalles tecnológicos
├── entry-points/ # Adaptadores de entrada (ej. API REST)
│ └── rest-api/
│ └── src/main/java/com/miempresa/infrastructure/entrypoints/producto/ProductoController.java
│ └── build.gradle.kts # Depende de 'domain/usecase'
│
└── driven-adapters/ # Adaptadores de salida (ej. Repositorio JPA)
└── jpa-repository/
│ └── src/main/java/com/miempresa/infrastructure/drivenadapters/producto/ProductoData.java # Entidad JPA
│ └── src/main/java/com/miempresa/infrastructure/drivenadapters/producto/ProductoRepositoryAdapter.java # Adaptador
│ └── build.gradle.kts # Depende de 'domain/model'
🗺️ Diagrama de Flujo y Dependencias
Este diagrama ilustra la Regla de la Dependencia (flechas sólidas de dependencia ->) y el Flujo de Control (flechas punteadas de ejecución ...> ). Observa cómo las dependencias siempre apuntan hacia el interior, hacia el dominio, mientras que el flujo de control atraviesa las capas.
+-------------------------------------------------------------------------------------------------+
| Capa de Aplicación (Ensamblador, main) |
+-------------------------------------------------------------------------------------------------+
|
| Inicia y configura
V
+-------------------------------------------------------------------------------------------------+
| Capa de Infraestructura (Adaptadores: REST, DB, etc.) <-- Las flechas de DEPENDENCIA apuntan aquí |
| |
| +---------------+ FLUJO DE CONTROL ...> +--------------------+ |
| | Entry-Point | | Driven-Adapter | |
| | (Controller) | ... ... ... ... ... ... ... ... | (JPA Repository) | |
| +---------------+ . +--------------------+ |
| | ^ . ^ | |
| (Llama) | (Retorna DTO) . (Implementa) | (Habla con DB) |
| | . . . . . | V |
| V | +---------------+ |
| <DEPENDENCIA> <DEPENDENCIA> | Mundo Externo | |
+------+------------------------------------------------------+----------+---------------+-------------+
| |
V V
+-------------------------------------------------------------------------------------------------+
| Capa de Dominio (Lógica de Negocio Pura) <-- TODAS las DEPENDENCIAS apuntan aquí |
| |
| +---------------+ FLUJO DE CONTROL ...> +--------------------+ |
| | Caso de Uso | | Repositorio (Port) | |
| | (Orquestador) | ... ... ... ... ... ... ... ... | (Interfaz) | |
| +---------------+ . +--------------------+ |
| ^ | . |
| | V . |
| | +----------+ |
| +-> | Agregado | <... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...|
| +----------+ |
+-------------------------------------------------------------------------------------------------+
💡 Ejemplo Avanzado: POST /orders (Crear un Pedido)
Veamos cómo fluyen las interacciones en un proceso de negocio real y complejo, como la creación de un nuevo pedido.
1. La Petición del Cliente (Capa de Infraestructura)
- Componente:
OrderController(Entry-Point). - Acción: Recibe una petición
POSTen/orders. Su rol es validar el formato de la petición (usando unPlaceOrderRequestDTO) y delegar inmediatamente al caso de uso correspondiente. No sabe cómo se procesa un pedido, solo a quién llamar. - Artefacto:
PlaceOrderRequestDTO(DTO). Modela el JSON de entrada (customerId,items, etc.). Usa validaciones de framework (@NotNull,@Size) para un rechazo temprano.
2. La Orquestación Central (Capa de Dominio)
- Componente:
PlaceOrderUseCase(Caso de Uso). - Acción: Este es el director de orquesta. No contiene lógica de negocio en sí mismo, pero coordina los pasos en el orden correcto. Su constructor recibe, mediante inyección de dependencias, varios puertos (interfaces):
CustomerRepository,ProductRepository,OrderPricingService,PaymentGateway, yOrderRepository.
3. Recolección y Validación de Negocio (Dominio interactuando con Infraestructura)
- Paso 3.1: Validar Cliente: El caso de uso invoca
customerRepository.findById(customerId). ElCustomerRepositoryAdapter(en infraestructura) lo buscará en la base de datos. Si no existe, el dominio lanza una excepción de negocio (CustomerNotFoundException). - Paso 3.2: Validar Productos y Stock: Para cada ítem, invoca
productRepository.findById(productId). El AgregadoProductrecuperado es responsable de validar sus propias reglas, comoproduct.hasSufficientStock(quantity).
4. Ejecución de Lógica de Negocio Compleja (Dominio)
- Componente:
OrderPricingService(Servicio de Dominio). - Acción: El caso de uso le pasa el cliente y los productos. Este servicio, cuya lógica no encaja en un único agregado, calcula el precio total, aplicando descuentos por lealtad o promociones. Devuelve un Objeto de Valor
Money.
5. Creación del Nuevo Agregado (Dominio)
- Componente:
Order(Agregado Raíz). - Acción: Con todos los datos validados y el precio calculado, el caso de uso invoca un método de fábrica estático:
Order.create(customer, items, totalPrice). El agregadoOrderse crea en un estado inicial válido y registra un evento,OrderPlacedEvent.
6. Interacción con Servicios Externos (Infraestructura)
- Componente:
PaymentGateway(Puertoen el dominio) yStripePaymentAdapter(Driven-Adapteren infraestructura). - Acción: El caso de uso llama a
paymentGateway.processPayment(...). El dominio solo conoce la interfaz. La infraestructura proporciona la implementación concreta (StripePaymentAdapter) que se comunica con la API de Stripe. Si el pago falla, se lanza una excepción que aborta el caso de uso.
7. Persistencia y Efectos Secundarios (Dominio y Infraestructura)
- Paso 7.1: Guardar el Pedido: Si el pago es exitoso, el caso de uso llama a
orderRepository.save(order). ElOrderRepositoryAdapter, usando unOrderDataMapperpara convertir el agregado a unOrderData(entidad JPA), persiste el pedido en la base de datos de forma transaccional. - Paso 7.2: Publicar Evento de Dominio: Tras guardar exitosamente, el caso de uso (o un decorador del repositorio) invoca a un
DomainEventPublisher. Esto despacha elOrderPlacedEventregistrado previamente. Otros módulos del sistema, comoNotificationsoInventory, pueden escuchar este evento y reaccionar de forma totalmente desacoplada (enviar un email, actualizar el stock).
8. La Respuesta Final (Infraestructura)
- Componente:
OrderController(de nuevo). - Acción: Recibe el resultado exitoso del caso de uso (el agregado
Orderrecién creado), lo mapea a unPlaceOrderResponseDTOy devuelve una respuesta201 Createdcon el ID del nuevo pedido.
🏁 Conclusión: Más Allá del Código
Adoptar una arquitectura basada en DDD y Hexagonal no es simplemente organizar carpetas; es un cambio de mentalidad. Nos obliga a dialogar con los expertos del negocio, a modelar la complejidad del mundo real y a proteger esa lógica invaluable de los detalles efímeros de la tecnología.
Los beneficios clave son innegables:
- Testabilidad Superior: La lógica de negocio pura en el dominio puede ser probada unitariamente sin necesidad de frameworks, bases de datos o servidores web.
- Mantenibilidad y Evolución: Cambiar de una base de datos PostgreSQL a MongoDB, o de una API REST a gRPC, se convierte en la tarea de escribir un nuevo adaptador, sin tocar el núcleo del negocio.
- Enfoque en el Negocio: El equipo se centra en resolver problemas de negocio reales, ya que el código refleja directamente el lenguaje y los procesos de la empresa.
- Escalabilidad Organizacional: Diferentes equipos pueden trabajar en distintos adaptadores o módulos del dominio de forma paralela con un bajo riesgo de conflictos.
Esta arquitectura sienta las bases para patrones aún más avanzados como CQRS (Command Query Responsibility Segregation) y Event Sourcing, permitiendo que tus sistemas no solo respondan a las necesidades actuales, sino que estén preparados para prosperar ante los desafíos del futuro.
Diseñando un Wrapper de Respuesta en Java con Funcionalidades de Optional y Gestión de Estado
- Mauricio ECR
- Snippets
- 30 Jun, 2025
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
Diseñando un Wrapper de Respuesta en Java con Funcionalidades de Optional y Gestión de Estado
- Mauricio ECR
- Snippets
- 30 Jun, 2025
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 externos— requiere estructuras robustas, claras y reutilizables. Aunque Optional<T> de Java es útil para representar valores potencialmente ausentes, su semántica está limitada a la presencia o ausencia de un valor, sin ofrecer un contexto de estado (como éxito, error, pendiente) ni información adicional como mensajes de error.
Este artículo tiene como objetivo presentar una implementación técnica detallada de una clase ResponseWrapper<T> en Java. Esta clase encapsula un valor de respuesta, un estado (Status) y una lista de errores, replicando y extendiendo las capacidades de Optional<T>. A través de esta herramienta, se busca proveer una estructura genérica que mejore la expresividad, manejabilidad y trazabilidad de las respuestas dentro de aplicaciones Java, especialmente en contextos de desarrollo backend, servicios REST, o flujos de validación de datos.
El contenido está orientado a desarrolladores de software, arquitectos de aplicaciones y diseñadores de APIs que deseen integrar una solución flexible y extensible para el manejo de respuestas estructuradas.
Implementación Técnica de ResponseWrapper
Motivación y Limitaciones de Optional<T>
El uso de Optional<T> es común para evitar null y sus efectos colaterales. Sin embargo, presenta limitaciones:
- No permite almacenar información contextual sobre por qué el valor está ausente.
- No diferencia entre un valor ausente por error y uno ausente por diseño (por ejemplo, un valor aún no calculado).
- No soporta transporte de metadatos como mensajes de error, códigos de estado, o indicadores de transición.
Por tanto, es útil extender su concepto en una clase personalizada que mantenga las siguientes características:
- Presencia opcional de un valor
- Estado de la operación (
SUCCESS,FAILURE,PENDING) - Listado de errores informativos o técnicos
- Soporte para operaciones tipo
map,flatMap,orElseyifPresent
Estructura de Código
La clase ResponseWrapper y el enum Status pueden definirse como sigue:
Archivo Status.java
public enum Status {
SUCCESS,
FAILURE,
PENDING
}
Archivo ResponseWrapper.java
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public class ResponseWrapper<T> {
private final T value;
private final Status status;
private final List<String> errors;
private ResponseWrapper(T value, Status status, List<String> errors) {
this.value = value;
this.status = status;
this.errors = errors != null ? new ArrayList<>(errors) : new ArrayList<>();
}
public static <T> ResponseWrapper<T> of(T value) {
return new ResponseWrapper<>(value, Status.SUCCESS, null);
}
public static <T> ResponseWrapper<T> empty() {
return new ResponseWrapper<>(null, Status.PENDING, null);
}
public static <T> ResponseWrapper<T> ofError(List<String> errors) {
return new ResponseWrapper<>(null, Status.FAILURE, errors);
}
public boolean isPresent() {
return value != null;
}
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
public void ifPresent(Consumer<? super T> consumer) {
if (value != null) {
consumer.accept(value);
}
}
public <U> ResponseWrapper<U> map(Function<? super T, ? extends U> mapper) {
if (!isPresent()) {
return empty();
}
return ResponseWrapper.of(mapper.apply(value));
}
public <U> ResponseWrapper<U> flatMap(Function<? super T, ResponseWrapper<U>> mapper) {
if (!isPresent()) {
return empty();
}
return mapper.apply(value);
}
public Status getStatus() {
return status;
}
public List<String> getErrors() {
return new ArrayList<>(errors);
}
public boolean isSuccess() {
return status == Status.SUCCESS;
}
public boolean isFailure() {
return status == Status.FAILURE;
}
public boolean isPending() {
return status == Status.PENDING;
}
@Override
public String toString() {
return value != null
? String.format("ResponseWrapper[%s, %s, %s]", value, status, errors)
: String.format("ResponseWrapper.empty[%s, %s]", status, errors);
}
}
Aplicaciones Prácticas
Caso de Uso 1: Servicio RESTful
En una API REST, ResponseWrapper puede encapsular una respuesta sin tener que lanzar excepciones para errores esperados:
@GetMapping("/usuarios/{id}")
public ResponseEntity<ResponseWrapper<Usuario>> obtenerUsuario(@PathVariable Long id) {
Optional<Usuario> usuario = usuarioService.buscarPorId(id);
if (usuario.isPresent()) {
return ResponseEntity.ok(ResponseWrapper.of(usuario.get()));
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ResponseWrapper.ofError(List.of("Usuario no encontrado")));
}
}
Caso de Uso 2: Validación de datos
public ResponseWrapper<String> validarEntrada(String input) {
if (input == null || input.isBlank()) {
return ResponseWrapper.ofError(List.of("Entrada vacía o nula"));
}
return ResponseWrapper.of(input.trim());
}
Caso de Uso 3: Procesamiento Encadenado
ResponseWrapper<String> resultado = validarEntrada(" hola ")
.map(String::toUpperCase)
.flatMap(this::procesarTexto);
if (resultado.isFailure()) {
log.warn("Errores: {}", resultado.getErrors());
}
Conclusiones
El patrón ResponseWrapper representa una evolución práctica del uso de Optional<T> en Java, permitiendo no solo modelar valores opcionales, sino también asociar metainformación esencial como estado y errores. Esta estructura permite escribir código más legible, declarativo y resiliente ante fallos predecibles.
Su versatilidad lo hace útil en diversos escenarios: servicios web, validaciones, transformaciones funcionales y pruebas. Además, su diseño extensible admite futuras adaptaciones como códigos de error tipados, trazabilidad de auditoría o integración con frameworks de serialización JSON.
Referencias y Recursos Adicionales
Auditoría: Un Enfoque Auto-Declarativo
- Mauricio ECR
- Auditoria
- 28 Jun, 2025
La Necesidad de Logs que Cuentan Historias En el ciclo de vida de cualquier plataforma digital, llega un momento crucial en el que responder a la pregunta "¿Quién hizo qué y cuándo?" deja de ser u
Auditoría: Un Enfoque Auto-Declarativo
- Mauricio ECR
- Auditoria
- 28 Jun, 2025
La Necesidad de Logs que Cuentan Historias
En el ciclo de vida de cualquier plataforma digital, llega un momento crucial en el que responder a la pregunta "¿Quién hizo qué y cuándo?" deja de ser una opción y se convierte en una necesidad. Los logs de auditoría son la crónica de nuestra aplicación, un registro inmutable de las acciones significativas.
Esta guía presenta la versión 0.1 de nuestros lineamientos de auditoría, un enfoque diseñado con un objetivo primordial: la legibilidad inmediata. Partimos de un principio fundamental que guiará todas las decisiones en esta fase inicial:
Principio Fundamental V0.1: Cada entrada de log debe ser una historia completa y comprensible por sí misma, sin requerir consultas a sistemas externos o la decodificación de identificadores (
ID) opacos.
El alcance de esta versión se centra deliberadamente en registrar acciones realizadas por usuarios autenticados (logueados). Los eventos anónimos o puramente sistémicos (ej. "base de datos iniciada") quedan fuera de este marco inicial para mantener la simplicidad y el enfoque.
2. ⚠️ El Compromiso Central de la V0.1: Legibilidad vs. Exposición de Datos
Para alcanzar la meta de logs auto-declarativos, la V0.1 adopta una postura de diseño que representa un compromiso consciente. Nos desviamos temporalmente de la práctica de seguridad estándar de minimizar la exposición de datos para maximizar la utilidad inmediata.
Al implementar esta guía, la organización acepta y gestiona conscientemente el siguiente riesgo:
Aceptación de Riesgo: Para facilitar la identificación inequívoca del actor sin consultas externas, se registrará información de identificación personal (PII), como el nombre completo y el correo electrónico del usuario, en texto plano dentro de los logs de auditoría. Esta decisión aumenta el riesgo inherente en caso de una brecha de seguridad que exponga dicho sistema de logs.
En consecuencia, el acceso a los repositorios de logs de auditoría debe ser considerado un privilegio extremadamente elevado. Dicho acceso debe estar protegido por múltiples capas de seguridad, ser rigurosamente controlado mediante listas de acceso explícitas y ser monitoreado de forma continua.
3. Anatomía del Evento de Auditoría (UserAuditEvent)
Cada evento de auditoría generado debe ser una instancia del siguiente modelo JSON. La estructura está optimizada para la descriptividad humana, favoreciendo campos claros sobre códigos crípticos.
{
"eventTimestamp": "2025-06-27T17:22:35.123Z",
"user": {
"email": "[email protected]",
"name": "Carlos Pérez",
"role": "Administrador"
},
"action": {
"type": "DOCUMENT_DOWNLOADED",
"description": "El usuario descargó el reporte financiero correspondiente al Q2 2025."
},
"resource": {
"type": "Reporte Financiero",
"name": "Reporte Financiero Q2 2025.pdf"
},
"context": {
"clientIp": "203.0.113.55",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
"details": {
"pageUrl": "/dashboard/reports",
"downloadId": "b1b2a38c-1e24-4a2b-8c6c-a9e9e1f2f3f4"
}
}
}
3.1. Desglose de Campos
| Campo | Descripción y Contenido Esperado | Razón de Ser |
|---|---|---|
eventTimestamp |
Fecha y hora UTC del evento en formato ISO 8601. Obligatorio. | Establece una línea de tiempo universal e inequívoca para todos los eventos. |
user |
Objeto que describe al actor. Prohibido el uso de IDs internos en esta V0.1. | Identifica al "quién" de la historia de forma inmediata. |
user.email |
Correo electrónico principal del usuario que realizó la acción. | Es el identificador humano único más común en sistemas digitales. |
user.name |
Nombre completo del usuario para una rápida identificación visual. | Aporta contexto humano y reduce la carga cognitiva del revisor. |
user.role |
El rol o perfil del usuario en el instante de la acción (ej. "Cliente", "Admin"). | Ayuda a evaluar si la acción era apropiada para el nivel de permisos del actor. |
action |
Objeto que describe la acción realizada, el "qué" de la historia. | Es el verbo de la narrativa del log. |
action.type |
Clasificador de la acción según la taxonomía definida. Obligatorio. | Permite la búsqueda, el filtrado y la creación de alertas automatizadas. |
action.description |
Descripción legible por humanos. Debe ser clara, concisa y completa. Obligatorio. | Es el corazón del principio auto-declarativo. Debe contar la historia. |
resource |
Objeto que describe el recurso o entidad afectada, el "sobre qué". | Aporta el objeto directo de la acción, completando la frase narrativa. |
resource.type |
Tipo de recurso en lenguaje natural (ej. "Factura", "Proyecto", "Usuario"). | Clasifica el objeto afectado para facilitar el análisis. |
resource.name |
Nombre o identificador legible del recurso (ej. "Factura #12345", "Proyecto Alpha"). | Especifica la instancia exacta del recurso que fue alterada. |
context |
Información técnica y ambiental para entender el "cómo" y el "dónde". | Proporciona pistas cruciales para investigaciones de seguridad o depuración. |
context.clientIp |
Dirección IP de origen de la solicitud. Esencial para análisis de seguridad. | Permite geolocalizar el origen de la acción y detectar patrones anómalos. |
context.userAgent |
Cadena del agente de usuario del cliente (navegador, app móvil, etc.). | Ayuda a identificar el tipo de dispositivo o software utilizado. |
context.details |
Objeto flexible para datos de contexto adicionales que completen la historia. | Un "cajón de sastre" para información valiosa que no encaja en otros campos. |
4. Taxonomía de Acciones: Un Vocabulario Controlado (action.type)
Para garantizar la consistencia y permitir un análisis de datos efectivo, todas las acciones deben clasificarse utilizando la siguiente taxonomía plana. La elección de un action.type correcto es fundamental.
action.type |
Cuándo Usarlo |
|---|---|
USER_LOGIN_SUCCESS |
Un usuario se autentica exitosamente en la plataforma. |
USER_LOGIN_FAILURE |
Falla un intento de inicio de sesión (por contraseña incorrecta, usuario inexistente, etc.). |
USER_LOGOUT |
Un usuario finaliza su sesión de forma explícita. |
PROFILE_DATA_UPDATED |
El usuario modifica la información de su propio perfil (ej. nombre, teléfono). |
PASSWORD_UPDATED |
El usuario completa exitosamente un cambio de su propia contraseña. |
DOCUMENT_VIEWED |
Se visualiza o se accede al contenido de un documento, archivo o reporte. |
DOCUMENT_DOWNLOADED |
Se inicia la descarga de un documento o archivo a un dispositivo local. |
ENTITY_CREATED |
Creación de un nuevo recurso de negocio (ej. un proyecto, una tarea, un cliente). |
ENTITY_UPDATED |
Modificación de un recurso de negocio existente. |
ENTITY_DELETED |
Eliminación (lógica o física) de un recurso de negocio. |
ADMIN_ACTION |
Acción privilegiada que afecta a otros usuarios o a la configuración global del sistema. |
5. El Arte de la Descripción: Creando Logs que Cuentan Historias
El campo action.description es el pilar de la legibilidad. Su propósito es verbalizar el evento de una manera que sea inmediatamente comprensible para un ser humano. Los demás campos actúan como datos estructurados que apoyan y enriquecen esta narrativa.
Ejemplo 1: Un administrador reasigna un rol.
La descripción debe ser la protagonista, explicando la acción de forma concisa.
{
"eventTimestamp": "2025-06-27T18:10:00Z",
"user": { "email": "[email protected]", "name": "Admin General", "role": "Superadmin" },
"action": {
"type": "ADMIN_ACTION",
"description": "El administrador reasignó el rol del usuario '[email protected]' de 'Editor' a 'Administrador'."
},
"resource": { "type": "Cuenta de Usuario", "name": "[email protected]" },
"context": {
"clientIp": "203.0.113.100",
"userAgent": "Mozilla/5.0...",
"details": { "adminPanelUrl": "/admin/users/edit/ana.gomez" }
}
}
Ejemplo 2: Un cliente actualiza campos específicos de su perfil.
Note cómo details enriquece la descripción sin sobrecargarla.
{
"eventTimestamp": "2025-06-27T19:30:15Z",
"user": { "email": "[email protected]", "name": "Juan Rodríguez", "role": "Cliente" },
"action": {
"type": "PROFILE_DATA_UPDATED",
"description": "El usuario actualizó su dirección de facturación personal."
},
"resource": { "type": "Perfil de Usuario", "name": "Juan Rodríguez" },
"context": {
"clientIp": "198.51.100.20",
"userAgent": "Mozilla/5.0...",
"details": { "changedFields": ["addressLine1", "city", "postalCode"] }
}
}
6. Disciplinas y Anti-Patrones de la V0.1
Incluso en una primera versión simplificada, la disciplina es clave para no generar ruido en lugar de señales.
Protección de Credenciales Sensibles: Categóricamente Prohibido. Nunca, bajo ninguna circunstancia, se deben registrar contraseñas, tokens de sesión, claves de API o cualquier otro secreto en texto plano. La descripción debe indicar el evento ("El usuario cambió su contraseña"), no el valor del secreto.
Principio de Concisión Selectiva: Evitar el Vuelco de Datos. El campo
context.detailses para añadir contexto, no para volcar objetos enteros de la base de datos o payloads de API completos. Seleccione manualmente 2 o 3 piezas de información clave que realmente aporten valor a la historia del log.La Plaga de la Generalidad: Exigir Descripciones Específicas. Una
action.descriptioncomo "Entidad actualizada" o "Registro modificado" es inaceptable por su inutilidad. Debe ser específica: "El usuario actualizó el número de teléfono del contacto 'Juan Pérez'" o "Se cambió el estado del proyecto 'Alpha' a 'Completado'".
7. Conclusión: La Hoja de Ruta hacia la V1.0
La versión 0.1 de estos lineamientos es un paso táctico y deliberado. Su objetivo es entregar valor inmediato a los equipos de seguridad, soporte y producto, proporcionando una trazabilidad clara y legible desde el primer día. Es un fundamento sólido sobre el cual construir.
La evolución natural hacia una versión 1.0, más robusta y segura, debe contemplar la siguiente hoja de ruta:
- Minimizar la Exposición de PII: El paso más crítico será reemplazar los datos personales explícitos (
user.email,user.name) por identificadores únicos y estables (user.id). Esta transición es fundamental para alinearse con las mejores prácticas de seguridad y requerirá el desarrollo de una herramienta interna segura que permita a personal autorizado resolver estos IDs. - Industrializar la Generación de Logs: Introducir un SDK de Auditoría o una librería centralizada. Esto estandarizará la creación de eventos, reducirá errores de implementación en los distintos servicios y facilitará futuras actualizaciones del formato del log.
- Enriquecer el Contexto para la Observabilidad: Incorporar identificadores de correlación (
traceId,requestId) en elcontext. Esto permitirá vincular un evento de auditoría específico con los logs de aplicación, métricas y trazas correspondientes, creando una visión de 360 grados de cada solicitud. - Formalizar y Escalar la Taxonomía: A medida que la plataforma crezca, la taxonomía plana actual deberá evolucionar. Un modelo jerárquico (ej.
Domain.Resource.ActioncomoUSER_MANAGEMENT.ACCOUNT.ROLE_CHANGED) permitirá una organización más granular y potente de los miles de eventos futuros.
Estos lineamientos V0.1 no son el destino final, sino el primer y más importante paso en el camino hacia una cultura de auditoría y responsabilidad madura.