Capas de Arquitectura: Entidades y Casos de Uso
Las Entidades contienen reglas de negocio críticas que existen en toda la empresa; los Casos de Uso orquestan flujos específicos de la aplicación y están aislados de los cambios de UI y base de datos.
Por qué importa
Las Entidades encapsulan reglas de negocio críticas a nivel empresarial. La fórmula de cálculo de intereses de un préstamo, la regla de que un pedido no puede cancelarse después del envío, la regla de que un paciente no puede tener dos citas al mismo tiempo — estas existen en el mundo real independientemente del software. Las Entidades son los objetos más estables y reutilizables del sistema.
Los Casos de Uso encapsulan reglas de negocio específicas de la aplicación. Orquestan el flujo de datos hacia y desde las Entidades para lograr el objetivo del caso de uso. "Solicitar un Préstamo" es un Caso de Uso: valida la entrada, verifica la puntuación crediticia, llama a la Entidad Préstamo para calcular los intereses y persiste el resultado. Los Casos de Uso conocen las Entidades — no conocen HTTP, SQL ni ningún framework de UI.
La prueba de corrección: si tu Caso de Uso importa algo del framework web o del ORM de base de datos, ya no es un Caso de Uso — es un controlador. Un Caso de Uso real puede ejercitarse desde una prueba unitaria sin servidor en ejecución ni conexión a base de datos.
✗El problema
A single function that does HTTP validation, bank calculation, SQL queries, and HTML rendering — all layers fused into one untestable blob.
Bad
from flask import request, jsonify
import psycopg2
def process_loan_application():
data = request.json # HTTP layer mixed in
if not data.get("principal") or not data.get("rate"):
return jsonify({"error": "Missing fields"}), 400
conn = psycopg2.connect("dbname=bank")
cur = conn.cursor()
cur.execute("SELECT credit_score FROM applicants WHERE id = %s",
(data["applicant_id"],))
credit_score = cur.fetchone()[0]
if credit_score < 650:
return jsonify({"error": "Credit score too low"}), 422
interest = data["principal"] * data["rate"] * data["term"]
cur.execute("INSERT INTO loans (principal, rate, term) VALUES (%s,%s,%s)",
(data["principal"], data["rate"], data["term"]))
conn.commit()
return jsonify({"approved": True, "interest": interest}), 201
import { Request, Response } from "express";
import { Pool } from "pg";
const db = new Pool({ connectionString: process.env.DB_URL });
export async function processLoanApplication(req: Request, res: Response) {
const { applicantId, principal, rate, term } = req.body;
if (!principal || !rate) return res.status(400).json({ error: "Missing fields" });
const { rows } = await db.query(
"SELECT credit_score FROM applicants WHERE id = $1", [applicantId]
);
if (rows[0].credit_score < 650) return res.status(422).json({ error: "Credit too low" });
const interest = principal * rate * term;
res.status(201).json({ approved: true, interest });
}
✓La solución
A clean Loan Entity holds the rule; a Use Case orchestrates the flow through abstract interfaces — no HTTP, no SQL anywhere in the business logic.
Good
from dataclasses import dataclass
from decimal import Decimal
from abc import ABC, abstractmethod
@dataclass
class Loan:
principal: Decimal
rate: Decimal
term: int
def calculate_interest(self) -> Decimal:
return self.principal * self.rate * self.term
@dataclass
class ApplyForLoanRequest:
applicant_id: str
principal: Decimal
rate: Decimal
term: int
@dataclass
class ApplyForLoanResponse:
approved: bool
interest: Decimal
loan_id: str
class ApplicantRepository(ABC):
@abstractmethod
def get_credit_score(self, applicant_id: str) -> int: ...
class LoanRepository(ABC):
@abstractmethod
def save(self, loan: Loan) -> str: ...
class ApplyForLoanUseCase:
def __init__(self, applicants: ApplicantRepository, loans: LoanRepository):
self._applicants = applicants
self._loans = loans
def execute(self, req: ApplyForLoanRequest) -> ApplyForLoanResponse:
score = self._applicants.get_credit_score(req.applicant_id)
if score < 650:
return ApplyForLoanResponse(approved=False, interest=Decimal(0), loan_id="")
loan = Loan(req.principal, req.rate, req.term)
interest = loan.calculate_interest()
loan_id = self._loans.save(loan)
return ApplyForLoanResponse(approved=True, interest=interest, loan_id=loan_id)
# No HTTP. No SQL. Trigger from CLI, REST, or a message queue.
export class Loan {
constructor(readonly principal: number, readonly rate: number, readonly term: number) {}
calculateInterest(): number { return this.principal * this.rate * this.term; }
}
export interface ApplyForLoanRequest { applicantId: string; principal: number; rate: number; term: number; }
export interface ApplyForLoanResponse { approved: boolean; interest: number; loanId: string; }
export interface ApplicantRepository { getCreditScore(id: string): Promise<number>; }
export interface LoanRepository { save(loan: Loan): Promise<string>; }
export class ApplyForLoanUseCase {
constructor(
private readonly applicants: ApplicantRepository,
private readonly loans: LoanRepository,
) {}
async execute(req: ApplyForLoanRequest): Promise<ApplyForLoanResponse> {
const score = await this.applicants.getCreditScore(req.applicantId);
if (score < 650) return { approved: false, interest: 0, loanId: "" };
const loan = new Loan(req.principal, req.rate, req.term);
const interest = loan.calculateInterest();
const loanId = await this.loans.save(loan);
return { approved: true, interest, loanId };
}
}
// No Express. No pg. No HTML. Trigger from HTTP, CLI, or a message queue.
💡Conclusión clave
Las Entidades contienen las reglas que existen en el mundo real. Los Casos de Uso orquestan esas reglas para un escenario de aplicación específico. Ninguna capa conoce HTTP, SQL ni ningún framework. Si tu Caso de Uso importa un framework web, ha sido promovido a controlador — y tu arquitectura ha perdido su separación más importante.
🔧 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 tu Caso de Uso importa algo del framework web o del ORM de base de datos, ya no es un Caso de Uso — es un controlador.
✗ Tu versión