Ejemplos prácticos de Clean Architecture y Arquitectura Hexagonal

🕒 4 minutos de lectura

Ejemplos de Clean Architecture y Arquitectura Hexagonal

En el artículo anterior hablábamos de Clean Architecture y Arquitectura Hexagonal desde un punto de vista conceptual: qué problema resuelven, cuándo usarlas y cuándo no.

Pero hay algo que siempre ayuda más que cualquier teoría: ver ejemplos reales.

En este artículo voy a mostrar el mismo problema resuelto con ambos enfoques, usando ejemplos sencillos y realistas, sin frameworks concretos ni estructuras artificiales.

El objetivo no es que copies una carpeta u otra, sino que entiendas la idea detrás de cada arquitectura.

 

El problema que vamos a resolver

Imaginemos una aplicación muy común:

👉 Una tienda online donde se pueden crear pedidos

Las reglas básicas son simples:

  • Un pedido tiene varios productos.
  • El precio final es la suma de los productos.
  • El pedido se guarda en algún sistema de persistencia.

Nada más. Nada de microservicios, colas ni integraciones raras.

 

Ejemplo con Clean Architecture

Clean Architecture parte siempre de la misma pregunta:

¿Cuál es la lógica de negocio pura de este sistema?

Y empieza desde el centro, no desde el framework.

 

Dominio: la lógica de negocio

Aquí vive lo importante. No hay anotaciones, no hay infraestructura.

class Order {
  private List<Item> items;

  double calculateTotal() {
    return items.stream()
        .mapToDouble(Item::price)
        .sum();
  }
}


Esta clase:

  • No sabe cómo se guarda el pedido.
  • No sabe si hay una API REST.
  • No sabe qué framework se usa.

👉 Solo representa el negocio.

 

Caso de uso: qué hace el sistema

Ahora definimos qué acción puede realizar el sistema.

class CreateOrderUseCase {

  private final OrderRepository orderRepository;

  CreateOrderUseCase(OrderRepository orderRepository) {
    this.orderRepository = orderRepository;
  }

  void execute(Order order) {
    orderRepository.save(order);
  }
}

Puntos clave:

  • El caso de uso depende de una interfaz, no de una implementación.
  • No hay dependencias técnicas.
  • Es fácil de testear.

Infraestructura: un detalle intercambiable

Aquí es donde aparece la base de datos, framework, etc.

class JpaOrderRepository implements OrderRepository {
  public void save(Order order) {
    // Persistencia con JPA
  }
}

Este código puede cambiar sin tocar el dominio ni los casos de uso.

 

🧠 Qué aporta Clean Architecture en este ejemplo

  • La lógica está protegida.
  • Los tests son simples.
  • El framework no domina el diseño.
  • El sistema puede evolucionar con menos riesgo.

Ejemplo con Arquitectura Hexagonal

Ahora resolvemos el mismo problema, pero pensando de otra forma.

En Hexagonal, la pregunta clave es:

¿Cómo entra información al sistema y cómo sale?

 

El núcleo de la aplicación

class OrderService {

  void createOrder(Order order) {
    // lógica de negocio
  }
}

Este núcleo:

  • No sabe si alguien llama por HTTP.
  • No sabe si se guarda en una base de datos.
  • No depende de nada externo.

 

Puerto de entrada (Input Port)

Define qué se puede hacer desde fuera.

interface CreateOrderPort {
  void createOrder(Order order);
}

Adaptador de entrada (por ejemplo REST)

class OrderController {

  private final CreateOrderPort port;

  OrderController(CreateOrderPort port) {
    this.port = port;
  }

  void create(OrderDto dto) {
    port.createOrder(dto.toDomain());
  }
}

Aquí entra la información al sistema.

Puerto de salida (Output Port)

interface OrderPersistencePort {
  void save(Order order);
}

Adaptador de salida (DB, API externa, etc.)

class OrderJpaAdapter implements OrderPersistencePort {
  public void save(Order order) {
    // persistencia
  }
}

🧠 Qué aporta Hexagonal en este ejemplo

  • El núcleo está totalmente aislado.
  • Cambiar REST por eventos no afecta al negocio.
  • Cambiar la DB no rompe el sistema.
  • El diseño es muy claro a nivel de dependencias.

 

Ejemplo de lo que NO es ni Clean ni Hexagonal

Este código es muy común… y muy problemático:

@RestController
class OrderController {

  @Autowired
  OrderRepository repository;

  @PostMapping("/orders")
  void create(OrderEntity entity) {
    // lógica de negocio aquí
    repository.save(entity);
  }
}

Problemas:

  • El controlador hace demasiadas cosas.
  • La lógica depende de la persistencia.
  • No hay separación real.
  • Testear esto es difícil.

Este tipo de código aparece cuando:
👉 usamos el nombre del patrón, pero no su idea.

 

Lo que ocurre en proyectos reales (y está bien)

En la práctica, muchos sistemas bien diseñados:

  • Usan casos de uso claros (Clean Architecture)
  • Junto con puertos y adaptadores (Hexagonal)
  • Sin obsesionarse con nombres o carpetas exactas

Eso no es un error.
Eso es arquitectura madura.

 

Conclusión

Clean Architecture y Arquitectura Hexagonal no son recetas mágicas. Son formas distintas de pensar el mismo problema.

Si después de aplicar uno de estos enfoques consigues que:

  • Tu dominio esté protegido,
  • Las dependencias estén bien dirigidas,
  • El sistema pueda cambiar sin romperse,

entonces has elegido bien.

El nombre del patrón es lo de menos. La idea detrás, lo es todo.