Un sistema recibe la temperatura de una habitación y avisa cuando alcanza los 30 grados. La primera versión muestra el aviso en una consola. Después queremos utilizar una pantalla, guardar los avisos o enviarlos por otro canal. Si la comparación de temperatura está mezclada con cada forma de avisar, una regla muy pequeña empieza a viajar por demasiados sitios.
La arquitectura hexagonal, o de puertos y adaptadores, separa lo que hace la aplicación de la forma en que se conecta con el exterior. Vamos a verlo con esa alerta: recibir una temperatura, decidir si hay que avisar y entregar el mensaje a un colaborador.
Primero, qué hace la aplicación
La regla será esta: con una temperatura de 30 grados o más, emitir un aviso. Con 29 no se emite ninguno. El sistema procesa cada lectura por separado; si recibe dos lecturas de 31, produce dos avisos.
La regla necesita un número y una forma de entregar el aviso. Puede expresarse sin decidir si el número procede de un formulario, un sensor o un archivo, y sin elegir todavía dónde aparecerá el mensaje.
El artículo original de Alistair Cockburn plantea esa separación entre la aplicación y sus conexiones. El dibujo del hexágono representa varias conversaciones alrededor del núcleo. Lo relevante son esas fronteras, no que existan seis lados ni seis interfaces.
Un puerto expresa una conversación
Un puerto es el contrato de una operación que la aplicación ofrece o necesita. En el ejemplo hay dos:
interface CheckTemperature {
void check(int temperature);
}
interface Notifier {
void send(String message);
}
Una interface declara operaciones que otra clase se comprometerá a realizar. CheckTemperature dice «puedes entregarme una temperatura para que la examine». Notifier dice «puedes entregarme un mensaje para que lo envíe».
El primero es un puerto de entrada: permite que alguien ponga en marcha una operación de la aplicación. El segundo es un puerto de salida: expresa una colaboración que la aplicación necesita para continuar su trabajo.
Entrada y salida describen quién inicia la conversación. No significan que los datos solo puedan circular en una dirección: un puerto de salida también podría devolver un resultado. Aquí send() no devuelve ninguno porque solo necesitamos mostrar la entrega de un aviso.
Como explicamos en dependencias y contratos, una firma necesita un significado. temperature se expresa en grados Celsius; el umbral es inclusivo. Esas dos decisiones permiten saber qué comportamiento esperamos de check(30).
El núcleo aplica la regla
La clase TemperatureAlert implementa el puerto de entrada y utiliza el puerto de salida:
class TemperatureAlert implements CheckTemperature {
private Notifier notifier;
public TemperatureAlert(Notifier notifier) {
this.notifier = notifier;
}
public void check(int temperature) {
if (temperature >= 30) {
notifier.send("High temperature: " + temperature + " C");
}
}
}
implements CheckTemperature indica que esta clase cumple aquel contrato. El constructor recibe el colaborador que enviará los mensajes. El método compara la temperatura y, cuando alcanza el umbral, le entrega un texto.
Aquí están la condición y la decisión de avisar. No aparece ninguna instrucción para dibujar una pantalla, leer una petición o guardar un archivo. Si mañana el umbral cambia a 32, sabemos qué regla hay que revisar.
Hay un detalle importante: la clase conoce Notifier, pero no conoce una implementación concreta. Necesita que alguien sepa enviar el mensaje; el mecanismo queda detrás del contrato.
Los adaptadores conectan el contrato con algo concreto
Un adaptador traduce entre una conexión exterior y la conversación que entiende la aplicación. Empecemos por la salida: este adaptador escribe los mensajes en la consola.
class ConsoleNotifier implements Notifier {
public void send(String message) {
System.out.println(message);
}
}
La implementación hace algo concreto y comprobable: imprime el texto recibido. La condición de los 30 grados no aparece aquí porque le corresponde a TemperatureAlert.
Ahora representaremos una entrada que recibe una temperatura escrita como texto. Su trabajo es convertirla a un número y llamar a la aplicación:
class TextTemperatureInput {
private CheckTemperature temperatures;
public TextTemperatureInput(CheckTemperature temperatures) {
this.temperatures = temperatures;
}
public void receive(String text) {
int temperature = Integer.parseInt(text);
temperatures.check(temperature);
}
}
Integer.parseInt() transforma un texto como "31" en el número 31. Si llega "hot", la conversión falla y el núcleo no recibe una temperatura. Interpretar ese formato pertenece a la entrada; decidir qué hacer con el número pertenece a la aplicación.
Este método representa la adaptación de una entrada textual. Una entrada web también interpretaría la petición y prepararía su respuesta. Otra entrada podría entregar directamente el número leído de un dispositivo, utilizando el mismo puerto.
Conectar las piezas y seguir dos lecturas
El lugar donde construimos los objetos elige qué adaptador utilizar:
Notifier notifier = new ConsoleNotifier();
CheckTemperature alert = new TemperatureAlert(notifier);
TextTemperatureInput input = new TextTemperatureInput(alert);
input.receive("29"); // No message.
input.receive("31"); // "High temperature: 31 C"
La primera llamada convierte "29" a 29. El núcleo compara el número con 30 y termina sin pedir ningún envío. La segunda convierte "31", cumple la condición y acaba imprimiendo High temperature: 31 C.
Con "30" también habría aviso, porque la comparación es «mayor o igual». Seguir esos tres valores ayuda a comprobar la regla antes de añadir una integración más grande.
A este punto de ensamblaje también se le llama composición: elegimos objetos concretos y conectamos sus dependencias. El constructor permite proporcionar el colaborador; la decisión arquitectónica consiste en que el núcleo lo conoce mediante su contrato.
Sustituir una salida sin cambiar la regla
Para observar los avisos sin imprimirlos podemos usar otro adaptador. Esta vez los guarda en una colección:
class CollectingNotifier implements Notifier {
private List<String> messages = new ArrayList<>();
public void send(String message) {
messages.add(message);
}
public List<String> getMessages() {
return new ArrayList<>(messages);
}
}
send() añade el mensaje a la lista. getMessages() permite consultar una copia de esa lista. Sigue cumpliéndose el mismo contrato de salida; ha cambiado el destino del texto.
CollectingNotifier notifier = new CollectingNotifier();
CheckTemperature alert = new TemperatureAlert(notifier);
alert.check(29);
notifier.getMessages(); // []
alert.check(30);
notifier.getMessages(); // ["High temperature: 30 C"]
Aquí utilizamos directamente el puerto de entrada porque ya tenemos números. El primer valor deja vacía la colección; el segundo añade el aviso. La regla y su umbral permanecen exactamente en el mismo método.
Esta es una ventaja de diseñar para poder comprobar el comportamiento: podemos observar la decisión sin depender del canal definitivo. Después habrá que comprobar que el adaptador de ese canal cumple correctamente el contrato.
La llamada y la dependencia siguen caminos distintos

Las flechas continuas significan «utiliza este tipo». Las discontinuas significan «implementa este contrato». En ejecución, TemperatureAlert llama al objeto concreto que recibió: puede ser un ConsoleNotifier o un CollectingNotifier.
En su código solo aparece Notifier. Los adaptadores conocen esa interfaz y la cumplen. Esta dirección aplica la inversión de dependencias de SOLID: el mecanismo de salida se ajusta al contrato que necesita la aplicación.
El límite perdería sentido si Notifier devolviese un objeto propio de una biblioteca de correo y el núcleo tuviera que interpretarlo. También lo perdería si cada entrada decidiera por su cuenta cuándo la temperatura merece un aviso. La frontera depende del conocimiento que cruza, no de cuántas interfaces dibujamos.
Cuándo ayuda esta separación
Los puertos son útiles para conversaciones con significado: guardar información, consultar un reloj, enviar un aviso o recibir una operación. No hace falta crear una interfaz para cada método pequeño. El contrato de entrada de este ejemplo hace visible la conversación; un método público del caso de uso también puede ejercer de entrada en un diseño más sencillo.
Podemos organizar responsabilidades en capas y reunir la funcionalidad mediante cortes verticales. Hexagonal añade el criterio de separar el núcleo de sus conexiones. Clean Architecture desarrolla una dirección de dependencias muy cercana, con atención explícita a reglas y casos de uso.
La revisión útil se puede hacer con una pregunta concreta: si el aviso cambia de canal, ¿toca cambiar la comparación de temperatura? Si basta con elegir otro adaptador que cumpla Notifier, hemos separado dos decisiones que pueden evolucionar por motivos distintos.