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

Límites Arquitectónicos: Trazando Líneas

La arquitectura traza líneas entre lo que importa (reglas de negocio) y lo que no (detalles técnicos) — estas líneas protegen la lógica central del negocio de los cambios en herramientas externas.

Por qué importa

Trazar una línea significa decidir qué módulo conoce a cuál. Las líneas más importantes se trazan entre las reglas de negocio centrales y todo lo demás — bases de datos, frameworks, interfaces de usuario, APIs externas. Estos son detalles. Los detalles son cosas que pueden cambiar. Las reglas de negocio son cosas que no deben cambiar solo porque cambie un detalle.

Un cambio en el esquema de la base de datos nunca debería requerir tocar la lógica de negocio. Un cambio de REST a GraphQL nunca debería propagarse a un cálculo de descuentos. Una actualización de un servicio de correo a otro nunca debería causar un fallo en una prueba de una regla financiera. Cuando estas cosas ocurren — cuando una migración de esquema toca un caso de uso, o un cambio de API requiere modificar una entidad — significa que no se trazó una línea donde debía haberse trazado.

✗El problema

InvoiceService knows about three unrelated external systems — pandas, PostgreSQL, and SMTP — meaning four systems must be running to test a single business rule.

Bad

import pandas as pd
from sqlalchemy.orm import Session
from models import InvoiceORM
import smtplib, ssl

class InvoiceService:
    def __init__(self, db: Session, smtp_host: str):
        self.db, self.smtp = db, smtp_host

    def generate_and_send(self, client_id: str) -> None:
        invoices = self.db.query(InvoiceORM).filter(
            InvoiceORM.client_id == client_id, InvoiceORM.paid == False
        ).all()
        df = pd.DataFrame([{"id": i.id, "amount": i.amount} for i in invoices])
        df.to_excel(f"/tmp/{client_id}_invoices.xlsx", index=False)
        ctx = ssl.create_default_context()
        with smtplib.SMTP_SSL(self.smtp, 465, context=ctx) as s:
            s.login("user", "pass")
            s.sendmail("billing@co.com", client_id, "See attachment")

# To test: need DB, filesystem, and SMTP server all running.
# Switching from Excel to PDF: touch InvoiceService.
# Switching from SMTP to SendGrid: touch InvoiceService.
import { DataSource } from "typeorm";
import ExcelJS from "exceljs";
import nodemailer from "nodemailer";

export class InvoiceService {
  constructor(private ds: DataSource, private smtpConfig: SMTPConfig) {}

  async generateAndSend(clientId: string): Promise {
    const invoices = await this.ds.getRepository(InvoiceEntity)
      .find({ where: { clientId, paid: false } });
    const wb = new ExcelJS.Workbook();
    const ws = wb.addWorksheet("Invoices");
    ws.addRows(invoices.map(i => [i.id, i.amount]));
    const buffer = await wb.xlsx.writeBuffer();
    const transport = nodemailer.createTransport(this.smtpConfig);
    await transport.sendMail({ to: clientId, attachments: [{ content: buffer }] });
  }
}
// No line between business rules and the three technical details.
// Test requires TypeORM, ExcelJS, and nodemailer all configured.

✓La solución

Three abstract ports — one per technical detail — draw three lines. InvoiceService depends on abstractions only. Each detail is independently swappable.

Good

from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class Invoice:
    id: str
    client_id: str
    amount: float
    paid: bool

# Three ports — three lines at the architectural boundary
class InvoiceRepository(ABC):
    @abstractmethod
    def get_unpaid(self, client_id: str) -> list[Invoice]: ...

class ReportFormatter(ABC):
    @abstractmethod
    def format(self, invoices: list[Invoice]) -> bytes: ...

class EmailSender(ABC):
    @abstractmethod
    def send(self, to: str, attachment: bytes) -> None: ...

# Business rule — no imports of pandas, SQLAlchemy, or smtplib
class InvoiceService:
    def __init__(self, repo: InvoiceRepository,
                 formatter: ReportFormatter, mailer: EmailSender):
        self._repo, self._formatter, self._mailer = repo, formatter, mailer

    def generate_and_send(self, client_id: str) -> None:
        invoices = self._repo.get_unpaid(client_id)
        report   = self._formatter.format(invoices)
        self._mailer.send(client_id, report)

# Swap Excel→PDF: replace ReportFormatter only — InvoiceService unchanged.
# Swap SMTP→SendGrid: replace EmailSender only — InvoiceService unchanged.
export interface Invoice { id: string; clientId: string; amount: number; paid: boolean; }

// Three ports — three lines
export interface InvoiceRepository { getUnpaid(clientId: string): Promise; }
export interface ReportFormatter    { format(invoices: Invoice[]): Promise; }
export interface EmailSender        { send(to: string, attachment: Buffer): Promise; }

// Business rule — no typeorm, no exceljs, no nodemailer
export class InvoiceService {
  constructor(
    private repo:      InvoiceRepository,
    private formatter: ReportFormatter,
    private mailer:    EmailSender,
  ) {}

  async generateAndSend(clientId: string): Promise {
    const invoices = await this.repo.getUnpaid(clientId);
    const report   = await this.formatter.format(invoices);
    await this.mailer.send(clientId, report);
  }
}
// In tests: stub all three interfaces with in-memory implementations.
// In production: TypeOrmInvoiceRepository | ExcelReportFormatter | SendGridEmailSender

💡Conclusión clave

Una frontera arquitectónica es cualquier lugar donde puedes trazar una línea y decir: "Nada de este lado conoce nada del otro lado." Las reglas de negocio viven de un lado; bases de datos, frameworks y E/S viven del otro. La línea es una interfaz abstracta.

🔧 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: Un límite arquitectónico es cualquier lugar donde puedas trazar una línea y decir: 'Nada de este lado sabe nada del otro lado.'

✗ Tu versión

Límites Arquitectónicos: Trazando Líneas — CleanKata — CleanKata