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

Los Detalles: Base de Datos, Web y Frameworks

La base de datos, la capa web y los frameworks son mecanismos de entrega — un buen arquitecto postpone estas decisiones y mantiene las reglas de negocio independientes para evitar un matrimonio tecnológico costoso.

Por qué importa

La afirmación más provocadora de Robert Martin: la base de datos es un detalle. Las reglas de negocio no se preocupan por si los datos se almacenan en MySQL, MongoDB, un archivo plano o en memoria. El patrón Repository abstrae esto — la lógica de negocio llama a repo.save(order) y la implementación concreta decide cómo. Cambia MySQL por PostgreSQL reescribiendo una clase.

La web es un detalle. HTTP es un mecanismo de entrega. Los mismos Casos de Uso que sirven a una API REST podrían también servir a una CLI, un consumidor de cola de mensajes o un trabajo por lotes. Si tu Caso de Uso importa el framework web, no puede dispararse desde una cola de mensajes sin reescribirlo. Un Caso de Uso que acepta un DTO simple como entrada no se preocupa por quién lo llamó.

Los frameworks son herramientas — no te cases con ellos. La señal de un matrimonio con un framework: reemplazar el framework web requiere reescribir la lógica de negocio. Un sistema bien arquitecturado te permite intercambiar el framework reescribiendo solo una delgada capa adaptadora. La lógica de negocio no se toca porque nunca supo que el framework existía. Deberías poder describir toda tu lógica de negocio sin mencionar un solo framework, base de datos ni protocolo.

✗El problema

Business logic written in Django/Express — the framework is so deeply embedded that replacing it means rewriting the entire application, not just the adapter layer.

Bad

# views.py — business logic written IN Django
from django.http import JsonResponse
from django.views import View
from .models import Product  # Django ORM model used as domain entity

class CheckoutView(View):
    def post(self, request):
        cart_items = request.session.get("cart", [])  # Django session

        total = 0
        for item in cart_items:
            product = Product.objects.get(pk=item["id"])  # Django ORM
            if product.stock < item["qty"]:
                return JsonResponse({"error": f"{product.name} out of stock"}, status=422)
            total += product.price * item["qty"]

        if total > 100:
            total *= 0.9  # business rule tangled in view

        return JsonResponse({"total": str(total)})

# Move to FastAPI: rewrite views, models, forms, session handling.
# Business rules must be found and extracted from Django code.
# The framework has colonized every layer.
// Business logic written IN Express
import express, { Request, Response } from "express";
import { PrismaClient }               from "@prisma/client";

const prisma = new PrismaClient();
const app    = express();

app.post("/checkout", async (req: Request, res: Response) => {
  const { cartItems } = req.body;
  let total = 0;
  for (const item of cartItems) {
    const product = await prisma.product.findUnique({ where: { id: item.id } });
    if (!product || product.stock < item.qty) {
      return res.status(422).json({ error: \`\${product?.name} out of stock\` });
    }
    total += product.price * item.qty;
  }
  if (total > 100) total *= 0.9;  // business rule in handler
  res.json({ total: total.toFixed(2) });
});
// Switch to NestJS: rewrite the entire handler. Business rules are buried inside.

✓La solución

Domain entities and use cases are pure Python/TypeScript with zero framework imports. A thin adapter layer translates between HTTP and the Use Case. Switching the framework means rewriting only the adapter.

Good

# Domain and use case — pure Python, no Django
from dataclasses import dataclass
from decimal import Decimal

@dataclass
class CartItem:
    name: str
    price: Decimal
    qty: int
    stock: int

    def subtotal(self) -> Decimal: return self.price * self.qty

class CheckoutUseCase:
    THRESHOLD = Decimal("100.00")
    DISCOUNT  = Decimal("0.90")

    def execute(self, items: list[CartItem]) -> dict:
        for item in items:
            if item.stock < item.qty:
                raise ValueError(f"{item.name} out of stock")
        total = sum(item.subtotal() for item in items)
        if total > self.THRESHOLD:
            total *= self.DISCOUNT
        return {"total": total}

# Django adapter — the ONLY file that knows about Django
class CheckoutView(View):
    def post(self, request):
        repo     = DjangoCartRepository()
        items    = repo.get_cart_items(request.session.get("cart", []))
        try:
            result = CheckoutUseCase().execute(items)
            return JsonResponse(result)
        except ValueError as e:
            return JsonResponse({"error": str(e)}, status=422)

# Switching to FastAPI: rewrite only CheckoutView — CheckoutUseCase is unchanged.
// Domain and use case — pure TypeScript, no Express, no Prisma
export interface CartItem { name: string; price: number; qty: number; stock: number; }

export class CheckoutUseCase {
  private readonly THRESHOLD = 100;
  private readonly DISCOUNT  = 0.9;

  execute(items: CartItem[]): { total: number } {
    for (const item of items) {
      if (item.stock < item.qty) throw new Error(\`\${item.name} out of stock\`);
    }
    let total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
    if (total > this.THRESHOLD) total *= this.DISCOUNT;
    return { total };
  }
}

// Express adapter — the ONLY file that knows about Express and Prisma
import express from "express";
import { PrismaCartRepository } from "./PrismaCartRepository";
import { CheckoutUseCase }      from "./CheckoutUseCase";

const app     = express();
const useCase = new CheckoutUseCase();

app.post("/checkout", async (req, res) => {
  try {
    const items  = await new PrismaCartRepository().getCartItems(req.body.cartItems);
    const result = useCase.execute(items);
    res.json({ total: result.total.toFixed(2) });
  } catch (e: any) {
    res.status(422).json({ error: e.message });
  }
});
// Switch to NestJS: rewrite only the adapter. CheckoutUseCase is completely untouched.

💡Conclusión clave

Deberías poder describir toda tu lógica de negocio a una persona no técnica sin mencionar un solo framework, base de datos ni protocolo. La base de datos, la capa web y los frameworks son mecanismos de entrega — herramientas valiosas, pero detalles. Mantenlos en los bordes. Reglas de negocio en el centro, mecanismos de entrega en la frontera.

🔧 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: Deberías poder describir toda tu lógica de negocio a una persona no técnica sin mencionar un solo framework, base de datos o protocolo.

✗ Tu versión

Los Detalles: Base de Datos, Web y Frameworks — CleanKata — CleanKata