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