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

  1. El problema que resol l'hexagonal
  2. El nucli de domini
  3. Ports primaris i secundaris
  4. Adaptadors
  5. La regla de dependència i la inversió
  6. Exemple complet en Java: port + adaptador
  7. Avantatges i inconvenients
  8. Errors comuns i consells
  9. Exercicis
  10. Conclusió

  1. 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 --> EXT

La 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.

  1. 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 import de 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.

  1. 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.

  1. 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.

  1. 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 Nucleo

Això 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.

  1. 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:

  • ContratarPolizaService viu 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());
    }
}

  1. 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

  1. Errors Comuns i Consells

  • Colar dependències de framework en el nucli. Si veus un import org.springframework o javax.persistence dins 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 dominio no importa res d'infraestructura.

  1. 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.

  1. 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

Mòdul 2: Principis i Tàctiques de Disseny

Mòdul 3: Estils i Patrons Arquitectònics

Mòdul 4: Arquitectures Distribuïdes i Microserveis

Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria

Mòdul 6: Disseny Dirigit pel Domini (DDD)

Mòdul 7: Dades i Persistència

Mòdul 8: Arquitectura al Núvol i Desplegament

Mòdul 9: Qualitat, Seguretat i Observabilitat

Mòdul 10: Evolució, Governança i Casos Pràctics

© Copyright 2026. Tots els drets reservats