Implementación de Hexagonal Architecture en Angular para una mejor estructura
Hexagonal Architecture con Angular: Guía práctica y criterio técnico
Hexagonal Architecture con Angular no es una moda bonita para poner en un README. Es la forma de evitar que tu aplicación se convierta en un Frankenstein atado al framework. Aquí te explico por qué, cómo y cuándo aplicarla sin volverte un fanático de la abstracción.
Tiempo estimado de lectura: 3 min
- Aislamiento claro: separa la lógica de negocio (TypeScript puro) de Angular y la infraestructura usando puertos y adaptadores.
- Pruebas fiables: casos de uso sin TestBed permiten tests rápidos y menos fragilidad en CI.
- Control del cambio: abstrae donde hay probable evolución; evita sobreingeniería y abuso de interfaces.
- Arquitectura práctica: estructura por responsabilidad para facilitar reemplazos de adaptadores (Http, WebSocket, NgRx).
¿Qué es, en una línea? Aislar la lógica de negocio (TypeScript puro) del mundo exterior (Angular, Http, storage), usando puertos (contratos) y adaptadores (implementaciones).
Resumen rápido (lectores con prisa)
Patrón que separa dominio, aplicación e infraestructura para mantener la lógica de negocio independiente del framework. Útil cuando hay lógica cliente significativa y se busca testeo rápido. Implementación con puertos (contratos) y adaptadores (implementaciones concretas).
Hexagonal Architecture con Angular: las capas y el contrato de dependencia
Dominio (core)
Entidades y reglas. Nada de Angular, nada de RxJS. Tipo puro.
Aplicación
Casos de uso y puertos. Orquestan el dominio y definen contratos (interfaces o clases abstractas).
Infraestructura
Adaptadores concretos (HttpClient, NgRx, components). Aquí vive Angular.
Regla de oro: las capas exteriores conocen a las interiores; las interiores no conocen a las exteriores.
Referencia conceptual: Alistair Cockburn — Ports and Adapters (hexagonal)
Ejemplo mínimo y realista
Dominio puro
// domain/user.entity.ts
export class User {
constructor(public id: string, public email: string, public role: 'admin' | 'user') {}
isAdmin() { return this.role === 'admin'; }
}
Puerto (aplicación)
// application/ports/user.repository.port.ts
export abstract class UserRepositoryPort {
abstract findById(id: string): Promise;
abstract save(user: User): Promise;
}
Adaptador (infraestructura – Angular)
// infrastructure/adapters/http-user.repository.ts
@Injectable()
export class HttpUserRepository implements UserRepositoryPort {
constructor(private http: HttpClient) {}
findById(id: string) { return firstValueFrom(this.http.get(`/api/users/${id}`)); }
save(user: User) { return firstValueFrom(this.http.post('/api/users', user)); }
}
Inyección con Angular (config)
// app.config.ts
providers: [
{ provide: UserRepositoryPort, useClass: HttpUserRepository },
GetUserUseCase
]
Angular DI hace el puente. Más sobre DI en la doc oficial: Angular Dependency Injection
Testing: la ganancia real
Cuando los casos de uso son TypeScript puro, los tests son instantáneos y sin TestBed. No más correr un microclima Angular solo para comprobar una regla de negocio.
it('devuelve usuario por id', async () => {
const mockRepo = { findById: jest.fn().mockResolvedValue(fakeUser) };
const useCase = new GetUserUseCase(mockRepo as any);
expect(await useCase.execute('123')).toEqual(fakeUser);
});
Resultado: CI más rápido, menos fragilidad y menos magia negra en los tests.
Estructura recomendada (simple y práctica)
Mantén carpetas por responsabilidad, no por tecnología:
src/
├── domain/
├── application/
│ ├── ports/
│ └── use-cases/
└── infrastructure/
├── adapters/
└── ui/
Esto hace que mover o substituir adaptadores (pasar de Http a WebSocket, o de NgRx a Signals) sea una tarea controlada, no una reescritura.
Cuándo aplicarla (criterio técnico)
Aplica Hexagonal Architecture con Angular si:
- Tienes lógica importante en cliente (cálculos, reglas offline, validaciones complejas).
- Quieres tests rápidos y confiables.
- El producto vive años y el equipo crece.
- Necesitas desarrollar en paralelo con mocks estables.
No la apliques si
- Es un CRUD simple que solo pinta el backend.
- La prioridad es lanzar rápido con un equipo pequeño.
- El dominio todavía está cambiando cada sprint (prototipado extremo).
El patrón no te hace bueno; te sirve si resuelves un problema claro. Forzarlo es sobreingeniería.
Puntos de atención y antipatterns
- No conviertas cada función en una interfaz por paranoia. Aplica abstracción donde hay cambio probable.
- Evita traer RxJS al dominio para “beneficiarte” del stream — eso es acoplamiento. Usa adaptadores para transformar observables a promesas o tipos puros.
- No escondas lógica en componentes; los componentes deben orquestar, no contener reglas.
Cierre con acción
Si quieres, en el próximo artículo preparo:
- Un scaffold de proyecto Angular con carpeta hexagonal lista.
- Un checklist de decisiones (qué abstraer, cuándo usar InjectionToken vs clase abstracta).
Apúntate al newsletter de Dominicode para recibirlo y el starter con ejemplos de tests en Jest.
Esto no termina aquí. Si tu código se está volviendo difícil de probar o de migrar, la arquitectura es la palanca—y saber cuándo usarla es criterio.
FAQ
Patrón que aísla la lógica de negocio en el centro y separa las dependencias externas mediante puertos (contratos) y adaptadores (implementaciones concretas).
Permite mantener TypeScript puro en el dominio, reducir acoplamiento al framework y facilitar tests rápidos y menos frágiles.
Los puertos son interfaces o clases abstractas en la capa de aplicación; los adaptadores son implementaciones concretas en infraestructura (por ejemplo, un repositorio HTTP usando HttpClient).
Los casos de uso escritos en TypeScript puro no requieren TestBed ni dependencias Angular para ejecutarlos, lo que hace los tests más rápidos y confiables.
No conviene para CRUDs simples, equipos pequeños que priorizan velocidad de entrega, o durante prototipado extremo donde el dominio cambia constantemente.
Referencia conceptual: Alistair Cockburn — Ports and Adapters (hexagonal). Para DI en Angular: Angular Dependency Injection.
