La tienda cambia de base de datos y la regla que decide si un pedido se puede cancelar deja de compilar. El método no ejecutaba SQL, pero recibía un objeto del ORM. El almacenamiento había conseguido una silla en la reunión de negocio.
Clean Architecture propone que las dependencias del código apunten hacia las políticas que queremos proteger. La interfaz, la persistencia y los frameworks pueden conocer los casos de uso; los casos de uso no necesitan conocer esos detalles para expresar sus decisiones.
Qué representan los círculos
La presentación de Robert C. Martin distingue políticas de negocio, casos de uso, adaptadores y detalles externos. Los círculos expresan niveles de conocimiento y dependencia, no un número obligatorio de paquetes.
En nuestra tienda podemos reconocer cuatro responsabilidades. Order protege si se puede cancelar. CancelOrder coordina esa operación. Un controlador traduce una petición y un adaptador carga y guarda pedidos. El servidor web y la biblioteca SQL son herramientas utilizadas en el borde.
La regla de dependencias dice que el interior no debe nombrar detalles del exterior. Si el caso de uso importa un controlador para decidir el código HTTP, conoce una tecnología de entrada. Si la entidad necesita un objeto de sesión SQL para cambiar de estado, conoce la persistencia. Ambas decisiones dificultan ejecutar la regla con otros colaboradores.
No se trata de negar que existan la base de datos o HTTP. La aplicación tiene que conectarse con ellos. Interesa localizar ese conocimiento allí donde se traduce cada conversación.
La idea conecta con el principio de inversión de dependencias. El caso de uso define qué colaboración necesita y el adaptador se ajusta a ella. La utilidad se aprecia cuando una regla puede ejecutarse sin arrastrar el servidor y la persistencia que utiliza una entrada concreta.
De los círculos a las piezas del ejemplo
Podemos leer el reparto desde dentro hacia fuera:
- Regla del pedido,
Order: conoce sus estados y la operación de cancelar. - Recorrido de aplicación,
CancelOrder,OrdersyCancellationResult: expresa cómo cargar un pedido, pedir su cancelación, guardarlo y comunicar el resultado. - Traducción de entrada y persistencia,
CancellationControlleryMemoryOrders: sabe cómo llamar al caso de uso o cómo cumplir su contrato de almacenamiento. - Herramientas externas, fuera del programa didáctico: el servidor, el ORM o el controlador de base de datos de una implementación real.
El reparto permite revisar nombres y relaciones sin convertir cada responsabilidad en un proyecto separado. Orders se coloca junto al caso de uso porque expresa su necesidad de persistencia. Otro diseño podría situar un contrato de repositorio en el dominio si esa fuese su responsabilidad; lo esencial es que su definición no requiera conocer la implementación externa.
Separar la decisión de su recorrido
Este ejemplo usa Java 17 o posterior. Todos los bloques se pueden guardar juntos en CleanExample.java. El modelo solo contiene pedidos pendientes y cancelados; omitimos pagos, stock y pedidos enviados para concentrarnos en la dirección de dependencias.
Retomamos la cancelación de cortes verticales, con un modelo reducido e independiente. Aquel ejemplo incluía pedidos pagados y enviados para explicar dónde compartir la regla. Aquí introducimos carga y guardado, y simplificamos los estados para que se vea la nueva frontera. Los programas no son versiones intercambiables del mismo modelo.
enum OrderStatus { PENDING, CANCELLED }
final class Order {
private OrderStatus status = OrderStatus.PENDING;
void cancel() { status = OrderStatus.CANCELLED; }
OrderStatus status() { return status; }
}
interface Orders {
Order load(String orderId);
void save(String orderId, Order order);
}
record CancellationResult(String orderId, OrderStatus status) {}
record CancelOrder(Orders orders) {
CancellationResult execute(String orderId) {
Order order = orders.load(orderId);
order.cancel();
orders.save(orderId, order);
return new CancellationResult(orderId, order.status());
}
}
El caso de uso conoce Orders, un contrato definido según lo que necesita. Cargar y guardar son capacidades; el contrato todavía no dice cómo se implementan. CancellationResult contiene datos del resultado, sin cabeceras HTTP ni objetos de la base de datos.
Como en dependencias y contratos, hay que precisar algo más que los nombres. En este programa load() devuelve un pedido existente o lanza un error; no devuelve null. El identificador reconocido es ORDER-1. Si save() falla, no devolvemos un resultado de éxito. Una aplicación real necesitaría distinguir esos errores con tipos y respuestas adecuados.
La entidad toma una decisión pequeña: pasar a cancelado. El caso de uso ordena las colaboraciones. Aunque ambas piezas estén cerca, responden a preguntas distintas. Si mañana cancelación exige comprobar un plazo, la regla debe tener un dueño claro; repartirla entre el controlador y el repositorio solo escondería el problema.
El exterior sabe cómo traducir
Para ejecutar el caso necesitamos un almacén. Lo hacemos en memoria y mediante una copia, de modo que guardar siga siendo una operación explícita del ejemplo:
final class MemoryOrders implements Orders {
private Order stored = new Order();
public Order load(String orderId) {
if (!"ORDER-1".equals(orderId)) {
throw new IllegalArgumentException("Unknown order.");
}
Order copy = new Order();
if (stored.status() == OrderStatus.CANCELLED) copy.cancel();
return copy;
}
public void save(String orderId, Order order) {
if (!"ORDER-1".equals(orderId)) {
throw new IllegalArgumentException("Unknown order.");
}
stored = order;
}
}
record CancellationController(CancelOrder cancelOrder) {
String cancel(String orderId) {
CancellationResult result = cancelOrder.execute(orderId);
return result.orderId() + ":" + result.status();
}
}
final class CleanExample {
public static void main(String[] args) {
var orders = new MemoryOrders();
var controller = new CancellationController(new CancelOrder(orders));
System.out.println(controller.cancel("ORDER-1"));
}
}
La salida es ORDER-1:CANCELLED. El controlador construye una representación textual. Uno HTTP decidiría códigos y cuerpo; uno de consola podría escribir otra frase utilizando el mismo resultado. También podríamos introducir un presentador mediante un contrato si hubiera una transformación que lo justificase. No necesitamos ese intermediario para devolver dos campos.
El almacén es un sustituto didáctico de un adaptador de persistencia. El ejemplo no resuelve concurrencia ni delimita una transacción real. En una compra, cargar y guardar deben participar en una política de consistencia explícita; la regla de dependencias no crea esa garantía automáticamente.
El flujo de ejecución puede salir hacia fuera

Las flechas continuas representan uso de tipos y la discontinua, implementación. Cuando se ejecuta orders.save(), el control llega a MemoryOrders, situado fuera. Sin embargo, CancelOrder no importa esa clase: conoce el contrato Orders.
Esta distinción explica cómo puede salir una llamada sin que una dependencia del código salga hacia un detalle técnico. El ensamblaje construye el adaptador y lo entrega al caso de uso. Para sustituirlo, modificamos esa composición y comprobamos el nuevo adaptador.
También importa qué datos cruzan la frontera. Una interfaz que devuelve SqlOrderRow no basta si el caso de uso tiene que interpretar columnas o estados propios del ORM. El contrato debe ofrecer información con un significado que el interior pueda entender por sí mismo.
Cómo encaja con hexagonal y MVC
La arquitectura hexagonal destaca las conversaciones entre la aplicación y el exterior. Clean Architecture hace explícita la dirección hacia políticas y casos de uso. Comparten la inversión de dependencias y pueden dar lugar a código muy parecido. No hace falta mantener dos árboles distintos para poder poner ambos nombres en un documento.
Respecto al ejemplo de capas, la diferencia visible está en la relación con los datos: allí QuoteService nombraba la clase concreta Catalog; aquí CancelOrder nombra el contrato Orders y el adaptador lo implementa. Organizar por capas también puede incorporar esa inversión. La etiqueta elegida no garantiza por sí sola unas dependencias concretas.
MVC organiza la presentación en modelo, vista y controlador. Puede servir para construir una entrada, pero no establece por sí solo la frontera entre el negocio y la base de datos. Una aplicación puede usar MVC en la web y una organización como esta en sus casos de uso.
Cada separación tiene un coste de traducción. Si una pantalla solo modifica una etiqueta, replicar el dato en seis clases puede aumentar el trabajo sin proteger una política significativa. En una compra con reglas y varias entradas, mantener el caso de uso independiente suele ofrecer una ventaja más clara.
Mejorar una frontera sin reconstruir la tienda
Si la cancelación está mezclada con HTTP y SQL, podemos separar primero la decisión y proteger su comportamiento con pruebas. Después definimos la pequeña colaboración que necesita el caso de uso y desplazamos las traducciones al borde. Es el criterio de refactorizar conservando el comportamiento aplicado a una dependencia concreta.
Que el núcleo no importe el ORM no convierte una migración de datos en un cambio gratuito. Habrá que adaptar consultas, comprobar la persistencia y verificar las garantías de la operación. La frontera reduce el conocimiento que se propaga; el trabajo de integración sigue existiendo.
Para revisar el diseño, intenta ejecutar CancelOrder con MemoryOrders sin cargar el servidor ni el ORM. Después busca qué tendrías que cambiar para responder con JSON. Si esa modificación lleva al controlador y deja intacta la decisión de cancelar, la frontera está cumpliendo su función.