La Arquitectura Hexagonal, también conocida como Ports and Adapters, fue propuesta por Alistair Cockburn con el objetivo de crear aplicaciones independientes de frameworks, bases de datos, interfaces gráficas o cualquier elemento externo. Su principal motivación es aislar el dominio de las dependencias externas.
Componentes Clave
- Dominio (Core): Contiene la lógica de negocio pura. No depende de nada externo.
- Puertos (Ports): Interfaces que definen cómo se comunica el dominio con el exterior.
- Adaptadores (Adapters): Implementaciones concretas de los puertos, como controladores REST, repositorios, UIs, etc.
[REST Controller] [CLI]
\ /
[Entrada (Input Port)]
|
[Dominio]
|
[Salida (Output Port)]
/ \
[Base de Datos] [API externa]
En algunas ocasiones también se le llama a los 'Input Port' como 'Puertos Primarios' y a los 'Output Port' como 'Puertos Secundarios'. Pero no son más que nombres distintos para el mismo concepto.
Capas:
- Domain: Contiene el corazón del negocio: entidades, lógica pura e interfaces (puertos) que definen qué necesita el dominio para funcionar. Esta capa no conoce ni depende de ninguna tecnología, ni sabe nada sobre controladores, frameworks o bases de datos.
- Application: Orquesta los casos de uso: coordina las operaciones del dominio usando las interfaces (puertos) que este expone. Esta capa depende del dominio, pero no sabe nada de la infraestructura (como REST, JPA o controladores).
- Infraestructure: Contiene los adaptadores concretos que implementan lo que el dominio necesita (como persistencia o entrada de datos). Aquí viven los controladores REST y los repositorios JPA. Esta capa sí depende de las anteriores, ya que necesita conocer los puertos del dominio para implementarlos, pero el flujo de dependencias no va al revés.
Esta separación garantiza un código modular, desacoplado, fácil de probar y de evolucionar sin romper el núcleo del negocio.
Como norma se establece que las capas superiores pueden utilizar clases de las capas inferiores pero no al revés, dando por hecho que la capas ordenadas de inferior a superior son 'domain', 'application' e 'infraestructure'.
- En la capa de 'domain' no se debe utilizar ninguna clase de las capas superiores 'application' e infraestructure'.
- En la capa de 'application' sí se puede utilizar clases de la inferior capa 'domain' pero no de la capa superior 'infraestructure'.
- En la capa de 'infraestructure' como es la capa superior, puede utilizar clases de las capas inferiores 'domain' y 'application'.
Ejemplo en Java - Gestión de Tareas:
Vamos a crear una aplicación simple que gestiona tareas (ToDo). Usaremos Java puro sin frameworks (como SpringBoot), para centrarnos en la estructura.
Simularemos la B.D. con un 'ArrayList' en memoria.
Tampoco vamos a utilizar DDD (Domain Driven Design) para no complicar el ejemplo y centrarnos directamente en las ventajas que aporta la Arquitectura Hexagonal. El uso de DDD en la Arquitectura Hexagonal es opcional y se recomienda su uso cuando el dominio tiene bastante lógica. Cuando las entidades de dominio actúan casi en su totalidad como DTOs (Data Transfer Objects) y no tienen lógica, se suele obviar el uso de DDD y se dice que el proyecto tiene un Dominio Anémico.
Estructura de paquetes:
src/
└── main/
└── java/
└── net/atopecode/todo/
├── domain/
│ ├── model/
│ └── port/
├── application/
├── infrastructure/
│ ├── input/
│ └── output/
└── MainApplication.java
- Capa de Dominio:
package net.atopecode.todo.domain.model;
public class Task {
private String id;
private String description;
private boolean completed;
public Task(String id, String description, boolean completed) {
this.id = id;
this.description = description;
this.completed = completed;
}
// Getters y setters
public String getId() { return id; }
public String getDescription() { return description; }
public boolean isCompleted() { return completed; }
public void setCompleted(boolean completed) {
this.completed = completed;
}
}
domain/port/TaskRepository.java (Output Port)
package net.atopecode.todo.domain.port;
import net.atopecode.todo.domain.model.Task;
import java.util.List;
public interface TaskRepository {
void save(Task task);
List<Task> findAll();
}
2. Capa de Aplicación:
application/TaskService.java (Input Port)
package net.atopecode.todo.application;
import net.atopecode.todo.domain.model.Task;
import java.util.List;
public interface TaskService {
void createTask(String description);
List<Task> getAllTasks();
}
application/TaskServiceImpl.java
package net.atopecode.todo.application;
import net.atopecode.todo.domain.model.Task;
import net.atopecode.todo.domain.port.TaskRepository;
import java.util.List;
import java.util.UUID;
public class TaskServiceImpl implements TaskService {
private final TaskRepository repository;
public TaskServiceImpl(TaskRepository repository) {
this.repository = repository;
}
@Override
public void createTask(String description) {
Task task = new Task(UUID.randomUUID().toString(), description, false);
repository.save(task);
}
@Override
public List<Task> getAllTasks() {
return repository.findAll();
}
}
3. Capa de Infraestructura:
infrastructure/output/InMemoryTaskRepository.java
package net.atopecode.todo.infrastructure.output;
import net.atopecode.todo.domain.model.Task;
import net.atopecode.todo.domain.port.TaskRepository;
import java.util.ArrayList;
import java.util.List;
public class InMemoryTaskRepository implements TaskRepository {
private final List<Task> tasks = new ArrayList<>();
@Override
public void save(Task task) {
tasks.add(task);
}
@Override
public List<Task> findAll() {
return new ArrayList<>(tasks);
}
}
infrastructure/input/ConsoleAdapter.java
package net.atopecode.todo.infrastructure.input;
import net.atopecode.todo.application.TaskService;
import net.atopecode.todo.domain.model.Task;
public class ConsoleAdapter {
private final TaskService taskService;
public ConsoleAdapter(TaskService taskService) {
this.taskService = taskService;
}
public void run() {
taskService.createTask("Aprender Arquitectura Hexagonal");
taskService.createTask("Publicar en atopecode.net");
for (Task task : taskService.getAllTasks()) {
System.out.println("- " + task.getDescription());
}
}
}
4. Main:
package net.atopecode.todo;
import net.atopecode.todo.application.TaskService;
import net.atopecode.todo.application.TaskServiceImpl;
import net.atopecode.todo.domain.port.TaskRepository;
import net.atopecode.todo.infrastructure.input.ConsoleAdapter;
import net.atopecode.todo.infrastructure.output.InMemoryTaskRepository;
public class Main {
public static void main(String[] args) {
TaskRepository repository = new InMemoryTaskRepository();
TaskService taskService = new TaskServiceImpl(repository);
ConsoleAdapter console = new ConsoleAdapter(taskService);
console.run();
}
}
Conclusión
Este ejemplo muestra cómo aplicar Arquitectura Hexagonal en un proyecto Java sin frameworks, organizando las clases en:
- El dominio es independiente del resto. Contiene la lógica de negocio y las interfaces.
- Las interfaces definen contratos, no implementaciones. Orquesta los casos de uso del negocio.
- Las implementaciones concretas (adaptadores) se pueden cambiar sin tocar la lógica de negocio. Se comunica con el exterior (UI, persistencia, etc.).
Lo interesante es que puedes cambiar los adaptadores (por ejemplo, una consola por una API REST) sin modificar la lógica de negocio.