Skip to main content

Inicia sesión en CleanKata

Sigue tu progreso, gana XP y desbloquea todas las lecciones.

Al iniciar sesión aceptas nuestros Términos de uso y Política de privacidad.

Arquitectura Limpia70 XP7 min

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