La mayoría de los posts sobre arquitectura hexagonal hablan de mantenibilidad y testabilidad. Eso está bien, pero hay una dimensión que casi nadie menciona: la arquitectura hexagonal es, por diseño, una estrategia de reducción de superficie de ataque. Cuando trabajé integrando pasarelas de pago en sistemas con lógica de negocio acoplada a la infraestructura, entendí que el problema no era solo de estructura de código, era de exposición de riesgo.
El principio central: aislamiento como control de seguridad
La arquitectura hexagonal establece que el dominio no depende de nada externo. Desde una perspectiva de seguridad, esto es equivalente al principio de mínimo privilegio aplicado al código: cada capa solo conoce lo estrictamente necesario para cumplir su función.
En un sistema acoplado, una vulnerabilidad en un adaptador HTTP puede propagarse directamente a la lógica de negocio porque no hay frontera clara. En hexagonal, el dominio está aislado detrás de interfaces (puertos), lo que significa que un adaptador comprometido tiene acceso limitado y controlado.
Esto no es una metáfora, es Defense in Depth aplicado a la arquitectura de software.
Cómo se ve esto en Spring Boot
La estructura de paquetes no es solo organización, es un boundary de seguridad:
src/main/java/com/app/
├── domain/
│ ├── model/ # Entidades, value objects, reglas de negocio
│ └── port/ # Interfaces: lo único que el dominio expone
├── application/
│ └── usecase/ # Orquestación, validación de entrada, autorización
└── infrastructure/
├── persistence/ # Repositorios JPA (nunca acceden al dominio directamente)
├── rest/ # Controllers HTTP: primer punto de validación
└── client/ # Clientes externos: terceros confinados aquí
Los puertos como contratos de confianza cero: Una interfaz bien definida en domain/port actúa como un contrato de confianza cero. El dominio no necesita confiar en el adaptador concreto; confía en el contrato. Si el adaptador es reemplazado o comprometido, el dominio sigue operando con las mismas garantías.
// El dominio define el contrato, no la implementación
public interface PaymentGateway {
// Recibe un objeto de dominio puro, sin dependencias de infraestructura
PaymentResult process(Payment payment);
}
Validación en los casos de uso, no en los adaptadores: Un error común es delegar la validación al controller REST o al repositorio. Desde el punto de vista de seguridad, eso es confiar en que el adaptador sea honesto. La validación de invariantes de negocio debe vivir en application/usecase, donde el código es independiente del vector de entrada:
@Service
public class ProcessPaymentUseCase {
private final PaymentGateway paymentGateway;
private final PaymentRepository paymentRepository;
public PaymentResult execute(ProcessPaymentCommand command) {
// Validación aquí, no en el controller.
// Independiente de si la llamada viene de HTTP, una cola de mensajes o un test.
if (command.amount().isNegativeOrZero()) {
throw new InvalidPaymentException("El monto debe ser positivo");
}
Payment payment = Payment.create(command.amount(), command.currency());
return paymentGateway.process(payment);
}
}
Esto es relevante en seguridad porque un atacante puede intentar alcanzar la lógica de negocio por múltiples vectores: HTTP, mensajes en cola (SQS/RabbitMQ), eventos de scheduler, etc. Si la validación está solo en el controller HTTP, los otros vectores quedan expuestos.
El riesgo real del código acoplado
Cuando la infraestructura y el dominio están mezclados, el blast radius de una vulnerabilidad es máximo. Un ejemplo concreto: si un @Repository JPA está inyectado directamente en un controller, y ese controller tiene una vulnerabilidad de inyección o de lógica de autorización, el atacante tiene acceso directo a la capa de datos sin pasar por ningún control de negocio.
Con hexagonal, el camino forzado es:
HTTP Request -> Controller (adapter) -> UseCase (application) -> Port (contract) -> Repository (adapter)
Cada flecha es un punto donde se puede aplicar un control de seguridad: autenticación, autorización, validación de entrada, rate limiting, auditoría. No es posible saltarse capas porque las dependencias simplemente no existen.
El complemento en Angular: no confiar en el frontend
Desde el lado del cliente, la regla es la misma: el componente no debe acceder directamente a la API. Los servicios de Angular actúan como adaptadores que encapsulan la comunicación HTTP, pero también son el lugar correcto para centralizar el manejo de tokens, headers de seguridad y sanitización de respuestas:
@Injectable({ providedIn: 'root' })
export class PaymentApiService {
constructor(
private http: HttpClient,
private authService: AuthService // El adaptador conoce el mecanismo de auth
) {}
processPayment(payload: PaymentRequest): Observable<PaymentResponse> {
// El componente nunca maneja tokens directamente
return this.http.post<PaymentResponse>('/api/payments', payload).pipe(
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse): Observable<never> {
// Manejo centralizado: no exponer stack traces al usuario
return throwError(() => new PaymentError(error.status));
}
}
La regla de oro desde seguridad: nunca confiar en validaciones del frontend. Toda validación del cliente es UX, no seguridad. Los controles reales están en los casos de uso del backend.
Cuando trabajo con Vite + React, aplico la misma separación con custom hooks. El componente consume el hook, el hook es el único punto que conoce la API. Si hay que rotar credenciales, cambiar endpoints o agregar headers de seguridad, el cambio está en un solo lugar.
Auditoría y trazabilidad: el beneficio que nadie menciona
Un sistema bien estructurado con hexagonal facilita enormemente la auditoría de seguridad. Porque todos los casos de uso son clases concretas con nombres explícitos, es trivial agregar logging de auditoría en un solo punto:
@Aspect
@Component
public class AuditAspect {
@Around("execution(* com.app.application.usecase.*.*(..))")
public Object auditUseCase(ProceedingJoinPoint joinPoint) throws Throwable {
// Cada operación de negocio queda registrada con usuario, timestamp y parámetros
log.info("USE_CASE_EXECUTED: {} by user: {}",
joinPoint.getSignature().getName(),
SecurityContextHolder.getContext().getAuthentication().getName());
return joinPoint.proceed();
}
}
En un sistema acoplado, la lógica de negocio está dispersa y agregar auditoría implica tocar decenas de clases. En hexagonal, un aspecto sobre el paquete usecase cubre todo.
Tres impactos concretos en seguridad
- Reducción de superficie de ataque: Los adaptadores de infraestructura solo exponen lo que el puerto define. No hay acceso directo entre capas.
- Validación independiente del vector: Las reglas de negocio se validan en los casos de uso, sin importar si la llamada viene de HTTP, una cola de mensajes o un job programado.
- Blast radius controlado: Si un adaptador es comprometido (por ejemplo, una librería de terceros con una CVE), el daño está contenido en esa capa. El dominio y los otros adaptadores no se ven afectados directamente.
Cuándo NO aplicarla
No es la arquitectura correcta para todo. Un CRUD de tres endpoints sin lógica de negocio no necesita este nivel de separación. El overhead de definir puertos e interfaces tiene costo real.
Donde sí tiene sentido: sistemas que procesan pagos, datos personales o cualquier operación con implicaciones de compliance (PCI-DSS, SOC 2, ISO 27001). En esos contextos, la arquitectura hexagonal no es una preferencia de diseño, es casi un requerimiento.
Referencias
- Alistair Cockburn - Hexagonal Architecture (2005)
- OWASP - Defense in Depth
- Tom Hombergs - Get Your Hands Dirty on Clean Architecture
- NIST - Least Privilege Principle
- Spring Docs - Structuring Your Code

Top comments (0)