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

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

Capas de Arquitectura: Entidades y Casos de Uso — CleanKata — CleanKata