L'arquitectura hexagonal, proposada per Alistair Cockburn el 2005, també coneguda com a Ports i Adaptadors, va néixer per resoldre un problema crònic de l'arquitectura en capes: que el domini acabava depenent de la infraestructura (la base de dades, el framework web, els serveis externs). La idea central és brillant per la seva simplicitat: aïlla la lògica de negoci en un nucli que no sap res del món exterior, i connecta'l a aquest món mitjançant ports (interfícies) que s'endollen a adaptadors intercanviables. Així, pots canviar la base de dades, el framework o el protocol sense tocar ni una sola línia del teu domini. En aquesta lliçó entendrem el nucli, els ports primaris i secundaris, els adaptadors, i construirem un exemple complet en Java.
Contingut
- El problema que resol l'hexagonal
- El nucli de domini
- Ports primaris i secundaris
- Adaptadors
- La regla de dependència i la inversió
- Exemple complet en Java: port + adaptador
- Avantatges i inconvenients
- Errors comuns i consells
- Exercicis
- Conclusió
- El problema que resol l'hexagonal
En l'arquitectura en capes clàssica, les dependències apunten cap avall: presentació → negoci → persistència. Això significa que el negoci depèn de la persistència, i la base de dades acaba condicionant el model de domini.
L'arquitectura hexagonal inverteix la situació: col·loca el domini al centre i fa que tota la resta en depengui, mai al revés.
graph TB
subgraph Exterior
UI[Adaptador Web/UI]
CLI[Adaptador CLI/Test]
DB[(Adaptador Base de Dades)]
EXT[Adaptador Servei Extern]
end
subgraph Hexagono[Nucli de Domini]
PP[Ports Primaris]
APP[Lògica d'aplicació + Domini]
PS[Ports Secundaris]
PP --> APP --> PS
end
UI --> PP
CLI --> PP
PS --> DB
PS --> EXTLa figura de l'hexàgon és només simbòlica (no significa "sis costats"): representa que el nucli té múltiples punts de connexió amb l'exterior, tots a través de ports.
- El nucli de domini
El nucli conté la raó de ser de l'aplicació: les entitats, les regles de negoci i els casos d'ús. La seva característica definitòria:
- No importa res de l'exterior. Zero
importde Spring, JPA, HTTP, JDBC. Només Java pur (o el teu llenguatge) i les teves pròpies abstraccions. - És independent de frameworks. Podries compilar el domini sense que existeixi ni base de dades ni servidor web.
- És 100% testejable en aïllament. No necessita aixecar res.
// Domini pur: cap dependència d'infraestructura
package com.fiatc.dominio;
public class Poliza {
private final String cliente;
private final String riesgo;
private final double prima;
public Poliza(String cliente, String riesgo, double prima) {
if (prima <= 0) throw new IllegalArgumentException("Prima inválida");
this.cliente = cliente;
this.riesgo = riesgo;
this.prima = prima;
}
public double prima() { return prima; }
public String cliente() { return cliente; }
public String riesgo() { return riesgo; }
}Observa que Poliza valida el seu propi invariant (prima positiva) en el constructor i no coneix res extern. És un objecte de domini pur.
- Ports primaris i secundaris
Un port és una interfície que defineix una frontera del nucli. Hi ha dos tipus, segons en quina direcció flueix la conversa:
| Tipus de port | També anomenat | Qui l'utilitza | Qui l'implementa | Exemple |
|---|---|---|---|---|
| Primari (driving) | D'entrada / API | L'exterior crida el nucli | El nucli | "Contractar pòlissa" |
| Secundari (driven) | De sortida / SPI | El nucli crida l'exterior | Un adaptador extern | "Desar pòlissa", "Notificar" |
- Port primari: descriu què pot fer l'aplicació. El món exterior (un controlador, un test) l'invoca per demanar un cas d'ús. L'implementa el nucli.
- Port secundari: descriu què necessita l'aplicació de l'exterior (persistència, notificacions). El nucli el declara com a interfície i l'implementa un adaptador extern.
// Port PRIMARI (d'entrada): què ofereix l'aplicació
package com.fiatc.dominio.puertos.entrada;
import com.fiatc.dominio.Poliza;
public interface ContratarPolizaUseCase {
Poliza contratar(String cliente, String riesgo);
}
// Port SECUNDARI (de sortida): què necessita l'aplicació de l'exterior
package com.fiatc.dominio.puertos.salida;
import com.fiatc.dominio.Poliza;
public interface RepositorioPolizas {
Poliza guardar(Poliza poliza);
}El que és crucial: ambdues interfícies viuen dins del nucli. El nucli posseeix els seus ports. L'exterior s'adapta a elles, no al revés.
- Adaptadors
Un adaptador és el codi que connecta un port amb una tecnologia concreta. Hi ha dues famílies, simètriques als ports:
- Adaptadors primaris (driving): tradueixen una petició externa (HTTP, CLI, esdeveniment de cua, test) en una crida al port primari. Exemple: un
@RestController. - Adaptadors secundaris (driven): implementen un port secundari utilitzant una tecnologia concreta (JPA, JDBC, un client HTTP a un servei extern). Exemple: un repositori JPA.
graph LR
HTTP[Petició HTTP] --> AP[Adaptador primari\nPolizaController]
AP --> PP[Port primari\nContratarPolizaUseCase]
PP --> SVC[Servei d'aplicació]
SVC --> PS[Port secundari\nRepositorioPolizas]
PS --> AS[Adaptador secundari\nRepositorioPolizasJpa]
AS --> BD[(BD)]El gran avantatge: pots tenir diversos adaptadors per al mateix port. El port secundari RepositorioPolizas pot tenir un adaptador JPA en producció i un adaptador en memòria en els tests, sense que el nucli se n'assabenti.
- La regla de dependència i la inversió
La regla d'or de l'hexagonal: les dependències sempre apunten cap al nucli.
graph LR
Adaptadores[Adaptadors] -->|depenen de| Puertos[Ports]
Puertos -.viuen a.-> Nucleo[Nucli]
Adaptadores -. mai al revés .-x NucleoAixò s'aconsegueix amb el Principi d'Inversió de Dependències (la "D" de SOLID):
- El nucli declara la interfície
RepositorioPolizas(el que necessita). - L'adaptador JPA implementa aquesta interfície (com es compleix).
- En temps d'execució, s'injecta l'adaptador concret en el nucli.
Resultat: el flux de control va del nucli a l'adaptador (el nucli crida guardar), però la dependència de codi va de l'adaptador al nucli (l'adaptador implementa la interfície del nucli). Aquesta inversió és el que protegeix el domini.
- Exemple complet en Java: port + adaptador
Muntem el cas d'ús complet de "contractar pòlissa".
// 1) SERVEI D'APLICACIÓ: implementa el port primari i utilitza el secundari
package com.fiatc.dominio.aplicacion;
import com.fiatc.dominio.Poliza;
import com.fiatc.dominio.puertos.entrada.ContratarPolizaUseCase;
import com.fiatc.dominio.puertos.salida.RepositorioPolizas;
public class ContratarPolizaService implements ContratarPolizaUseCase {
private final RepositorioPolizas repositorio; // port secundari (interfície)
public ContratarPolizaService(RepositorioPolizas repositorio) {
this.repositorio = repositorio; // s'injecta un adaptador concret
}
@Override
public Poliza contratar(String cliente, String riesgo) {
double prima = calcularPrima(riesgo); // regla de negoci
Poliza poliza = new Poliza(cliente, riesgo, prima);
return repositorio.guardar(poliza); // crida el port de sortida
}
private double calcularPrima(String riesgo) {
return "ALTO".equals(riesgo) ? 1200 : 600; // lògica de domini pura
}
}Anàlisi:
ContratarPolizaServiceviu en el nucli i només coneix interfícies (RepositorioPolizas), mai tecnologies.- Rep el repositori per constructor (injecció de dependències): no el crea, l'hi donen.
// 2) ADAPTADOR PRIMARI: tradueix HTTP -> port primari
package com.fiatc.infraestructura.web;
import com.fiatc.dominio.puertos.entrada.ContratarPolizaUseCase;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/polizas")
class PolizaController {
private final ContratarPolizaUseCase useCase; // depèn del port, no del service
PolizaController(ContratarPolizaUseCase useCase) { this.useCase = useCase; }
@PostMapping
PolizaDto contratar(@RequestBody ContratarRequest req) {
var poliza = useCase.contratar(req.cliente(), req.riesgo());
return new PolizaDto(poliza.cliente(), poliza.prima()); // tradueix a DTO
}
}// 3) ADAPTADOR SECUNDARI: implementa el port de sortida amb JPA
package com.fiatc.infraestructura.persistencia;
import com.fiatc.dominio.Poliza;
import com.fiatc.dominio.puertos.salida.RepositorioPolizas;
import org.springframework.stereotype.Repository;
@Repository
class RepositorioPolizasJpa implements RepositorioPolizas {
private final JpaPolizaDao dao; // tecnologia concreta encapsulada aquí
RepositorioPolizasJpa(JpaPolizaDao dao) { this.dao = dao; }
@Override
public Poliza guardar(Poliza poliza) {
var entidad = PolizaEntity.desde(poliza); // mapatge domini -> entitat JPA
dao.save(entidad);
return poliza;
}
}I el cablejat (composició):
// 4) CONFIGURACIÓ: aquí s'"endollen" els adaptadors als ports
@Configuration
class Configuracion {
@Bean
ContratarPolizaUseCase contratarPolizaUseCase(RepositorioPolizas repo) {
return new ContratarPolizaService(repo); // injecta l'adaptador JPA
}
}La clau de l'exemple: el paquet com.fiatc.dominio no importa res de Spring ni de JPA. Tota la tecnologia viu a com.fiatc.infraestructura. Si demà canvies JPA per MongoDB, només escrius un nou adaptador secundari; el nucli no es toca.
I per provar el cas d'ús, ni tan sols necessites base de dades:
// TEST: adaptador secundari en memòria, sense Spring ni BD
class ContratarPolizaServiceTest {
@Test
void contrata_y_guarda() {
var enMemoria = new RepositorioPolizas() {
Poliza ultima;
public Poliza guardar(Poliza p) { this.ultima = p; return p; }
};
var service = new ContratarPolizaService(enMemoria);
var poliza = service.contratar("ACME", "ALTO");
assertEquals(1200, poliza.prima());
}
}
- Avantatges i inconvenients
| Avantatges | Inconvenients |
|---|---|
| El domini queda aïllat i lliure de frameworks | Més interfícies i classes (major "cerimònia") |
| Adaptadors intercanviables (JPA, Mongo, tests) | Corba d'aprenentatge per a l'equip |
| Tests del nucli sense infraestructura | Pot ser sobreenginyeria en CRUDs trivials |
| Inversió de dependències clara | Necessita disciplina per no "colar" tecnologia en el nucli |
| Facilita migrar tecnologies sense tocar el negoci | Mapatges domini↔entitat addicionals |
- Errors Comuns i Consells
- Colar dependències de framework en el nucli. Si veus un
import org.springframeworkojavax.persistencedins del domini, has trencat l'hexagonal. - Confondre port primari i secundari. Primari = l'exterior et crida (l'implementa el nucli). Secundari = tu crides l'exterior (l'implementa un adaptador).
- Utilitzar l'entitat JPA com a objecte de domini. Acobla el domini a la persistència. Mantén entitats de domini i de persistència separades, amb mapatge entre totes dues.
- Aplicar-la a tot. En un CRUD simple sense regles, l'hexagonal pot ser sobreenginyeria. Reserva l'esforç per a dominis rics.
- Consell: utilitza proves d'arquitectura (p. ex. ArchUnit) per verificar automàticament que el paquet
dominiono importa res d'infraestructura.
- Exercicis
Exercici 1. Classifica cada port com a primari o secundari: (a) EnviarNotificacion; (b) ConsultarSaldoUseCase; (c) RepositorioClientes; (d) PasarelaPago.
Exercici 2. El teu nucli necessita enviar un correu en contractar una pòlissa. Dissenya el port i anomena dos adaptadors possibles (un de producció i un de test).
Exercici 3. Explica per què el ContratarPolizaService es pot provar sense aixecar la base de dades.
Solucions
Solució 1. (a) Secundari (el nucli crida l'exterior per notificar). (b) Primari (l'exterior invoca un cas d'ús). (c) Secundari (persistència). (d) Secundari (servei extern de pagament).
Solució 2. Port secundari:
public interface NotificadorEmail {
void enviar(String destinatario, String asunto, String cuerpo);
}Adaptadors: (1) producció → NotificadorSmtp que envia per SMTP; (2) test → NotificadorEnMemoria que desa els correus en una llista per verificar-los.
Solució 3. Perquè ContratarPolizaService depèn de la interfície RepositorioPolizas, no de la seva implementació JPA. En el test se li injecta un adaptador en memòria, de manera que s'exercita tota la lògica de negoci sense tocar infraestructura.
- Conclusió
L'arquitectura hexagonal situa el domini al centre i el protegeix de l'exterior mitjançant ports (interfícies que posseeix el nucli) i adaptadors (implementacions intercanviables que depenen del nucli). Gràcies a la inversió de dependències, el negoci queda lliure de frameworks, és plenament testejable i permet canviar tecnologies sense tocar la lògica. Hem construït un exemple complet de port primari, port secundari i els seus adaptadors. Aquestes mateixes idees —domini al centre, dependències cap endins— són el fonament dels estils que veurem a continuació: l'Arquitectura Neta i l'Arquitectura Ceba (Clean & Onion), que formalitzen la regla de dependència en cercles concèntrics i que compararem amb l'hexagonal.
Curs d'Arquitectura d'Aplicacions
Mòdul 1: Fonaments de l'Arquitectura d'Aplicacions
- Què és l'Arquitectura d'Aplicacions?
- El Rol de l'Arquitecte de Programari
- Atributs de Qualitat i Requisits No Funcionals
- Decisions Arquitectòniques i Compromisos (Trade-offs)
- Documentació d'Arquitectura: Vistes i el Model C4
Mòdul 2: Principis i Tàctiques de Disseny
- Acoblament, Cohesió i Separació de Responsabilitats
- Principis SOLID Aplicats a l'Arquitectura
- DRY, KISS, YAGNI i Altres Principis de Disseny
- Tàctiques Arquitectòniques per als Atributs de Qualitat
- Gestió del Deute Tècnic
Mòdul 3: Estils i Patrons Arquitectònics
- Arquitectura Monolítica
- Arquitectura en Capes (N-Tier)
- Arquitectura Client-Servidor
- Arquitectura Hexagonal (Ports i Adaptadors)
- Arquitectura Neta i Ceba (Clean & Onion)
Mòdul 4: Arquitectures Distribuïdes i Microserveis
- Introducció als Sistemes Distribuïts
- Arquitectura de Microserveis
- Descomposició de Serveis i Bounded Contexts
- API Gateway, Service Discovery i Comunicació entre Serveis
- Patrons de Resiliència: Circuit Breaker, Retry i Bulkhead
- El Teorema CAP i la Consistència de Dades
Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria
- Fonaments de l'Arquitectura Orientada a Esdeveniments
- Missatgeria Asíncrona: Cues i Brokers
- Patrons d'Esdeveniments: Event Sourcing i CQRS
- Gestió de Transaccions Distribuïdes: Patró Saga
- Streaming de Dades en Temps Real
Mòdul 6: Disseny Dirigit pel Domini (DDD)
- Conceptes Fonamentals del DDD
- Disseny Estratègic: Bounded Contexts i Llenguatge Ubic
- Disseny Tàctic: Entitats, Agregats i Repositoris
- Mapatge de Contextos (Context Mapping)
Mòdul 7: Dades i Persistència
- Estratègies de Persistència: SQL vs NoSQL
- Patrons d'Accés a Dades: Repository, Unit of Work i DAO
- Base de Dades per Servei i Gestió de Dades Distribuïdes
- Cau i Estratègies d'Invalidació
Mòdul 8: Arquitectura al Núvol i Desplegament
- Fonaments del Cloud Computing (IaaS, PaaS, SaaS)
- Contenidors i Orquestració amb Docker i Kubernetes
- Arquitectura Serverless
- Patrons de Disseny Cloud-Native
- Infraestructura com a Codi (IaC)
Mòdul 9: Qualitat, Seguretat i Observabilitat
- Escalabilitat: Horitzontal vs Vertical i Balanceig de Càrrega
- Alta Disponibilitat i Tolerància a Fallades
- Seguretat per Disseny i Autenticació/Autorització
- Observabilitat: Logging, Mètriques i Traçabilitat
- Rendiment i Proves de Càrrega
