El Antipatrón Periférico
El antipatrón periférico ocurre cuando el código de infraestructura habla directamente con otro código de infraestructura, saltándose el dominio — como el anillo de París, el tráfico fluye alrededor de la ciudad en lugar de a través de ella.
Por qué importa
El antipatrón periférico recibe su nombre del anillo de carreteras de París (le périphérique): una ruta que permite al tráfico circular alrededor de la ciudad sin entrar nunca en ella. En software, el equivalente es un controlador que llama directamente a repositorios, colas de mensajes y servicios externos — saltándose los casos de uso del dominio por completo. La capa de dominio existe en papel pero no recibe tráfico.
Cómo sucede: Los repositorios son públicos. Nadie aplica que los controladores deben pasar por los casos de uso. Un desarrollador bajo presión de tiempo escribe un controlador que hace "solo esta llamada a base de datos" directamente. Luego otra, y otra más. Las reglas de negocio se acumulan en controladores y repositorios — no en casos de uso. La capa de casos de uso se convierte en un paso intermedio delgado o desaparece por completo.
La consecuencia: las reglas de negocio están dispersas. Para entender qué sucede cuando un usuario realiza un pedido, debes leer el controlador, tres repositorios y dos tareas en segundo plano. Nada de eso es testeable sin una base de datos activa y un servidor HTTP. La capa de dominio es vestigial — presente pero sin uso.
La solución: los controladores llaman a los casos de uso. Los casos de uso llaman a los repositorios (a través de puertos). Los repositorios son privados de paquete. El caso de uso es el único punto de entrada al dominio, y posee toda la lógica de negocio.
✗El problema
A controller makes three direct infrastructure calls with zero domain logic. Business rules are scattered across the controller. Use cases are empty. Domain layer is bypassed entirely.
Bad
from flask import request, jsonify
from orders.order_repository import OrderRepository
from payments.payment_repository import PaymentRepository
from emails.email_repository import EmailRepository
class OrderController:
def place_order(self):
data = request.json
order_repo = OrderRepository()
payment_repo = PaymentRepository()
email_repo = EmailRepository()
order = order_repo.find_by_id(data["order_id"]) # infrastructure
if order["status"] != "pending":
return jsonify({"error": "Order already processed"}), 400
if order["total"] > 1000: # business rule in controller!
payment_repo.flag_for_review(order["id"]) # infrastructure
payment_repo.charge(order["id"], order["total"]) # infrastructure
email_repo.send_confirmation(data["email"], order) # infrastructure
return jsonify({"status": "ok"})
# Controller: 20 lines. Use case: empty. Domain layer: bypassed.
# Testing requires real database, real payment gateway, and real SMTP server.
import { Request, Response } from "express";
import { OrderRepository } from "../orders/OrderRepository";
import { PaymentRepository } from "../payments/PaymentRepository";
import { EmailRepository } from "../emails/EmailRepository";
export class OrderController {
async placeOrder(req: Request, res: Response): Promise {
const { orderId, email } = req.body;
const order = await new OrderRepository().findById(orderId); // infrastructure
if (order.status !== "pending") {
res.status(400).json({ error: "Already processed" }); return;
}
if (order.total > 1000) { // business rule!
await new PaymentRepository().flagForReview(orderId); // infrastructure
}
await new PaymentRepository().charge(orderId, order.total); // infrastructure
await new EmailRepository().sendConfirmation(email, order); // infrastructure
res.json({ status: "ok" });
}
}
// Controller: 20 lines. Use case: empty. Domain layer: bypassed.
✓La solución
Controller calls one use case. The use case owns all business logic and orchestrates the repositories. Controller is 7 lines. Use case is fully testable with mock adapters.
Good
from flask import request, jsonify
from orders.place_order_use_case import PlaceOrderUseCase
class OrderController:
def place_order(self):
data = request.json
try:
result = PlaceOrderUseCase().execute(
order_id=data["order_id"], email=data["email"]
)
return jsonify(result)
except ValueError as e:
return jsonify({"error": str(e)}), 400
# All business logic in the use case
class PlaceOrderUseCase:
def __init__(self):
self._orders = _OrderRepository()
self._payments = _PaymentService()
self._emails = _EmailPort()
def execute(self, order_id: str, email: str) -> dict:
order = self._orders.find(order_id)
if order["status"] != "pending":
raise ValueError("Order already processed") # domain rule in domain
if order["total"] > 1000: # domain rule in domain
self._payments.flag_for_review(order_id)
self._payments.charge(order_id, order["total"])
self._emails.send_confirmation(email, order)
return {"status": "ok"}
# Controller: 7 lines. Use case: all the logic.
# Use case testable with mock adapters — no HTTP, no database required.
import { Request, Response } from "express";
import { PlaceOrderUseCase } from "../domain/PlaceOrderUseCase";
export class OrderController {
constructor(private readonly useCase: PlaceOrderUseCase) {}
async placeOrder(req: Request, res: Response): Promise {
try {
const result = await this.useCase.execute(req.body.orderId, req.body.email);
res.json(result);
} catch (e: any) {
res.status(400).json({ error: e.message });
}
}
}
// All business logic in the use case
export class PlaceOrderUseCase {
constructor(
private readonly orders: OrderRepository,
private readonly payments: PaymentService,
private readonly emails: EmailPort,
) {}
async execute(orderId: string, email: string): Promise<{ status: string }> {
const order = await this.orders.find(orderId);
if (order.status !== "pending") throw new Error("Order already processed");
if (order.total > 1000) await this.payments.flagForReview(orderId);
await this.payments.charge(orderId, order.total);
await this.emails.sendConfirmation(email, order);
return { status: "ok" };
}
}
// Controller: 7 lines. Use case: all the logic.
// Testable with InMemoryOrderRepository, MockPaymentService, MockEmailPort.
💡Conclusión clave
Si tus controladores son largos y tus casos de uso están vacíos, tu lógica de negocio ha escapado a la periferia. Como el anillo de carreteras de París, todo el tráfico fluye alrededor del dominio sin entrar nunca. La capa de dominio existe pero es irrelevante. Restaura la regla: los controladores llaman a los casos de uso, los casos de uso poseen las reglas de negocio, los repositorios implementan puertos — y haz los repositorios privados de paquete para que el controlador no tenga otra opción.
🔧 Algunos ejercicios pueden tener errores. Si algo parece incorrecto, usa el botón Feedback (abajo a la derecha) para reportarlo — nos ayuda a corregirlo rápido.
Pista: Si tus controladores son largos y tus casos de uso están vacíos, tu lógica de negocio ha escapado a la periferia.
✗ Tu versión