Skip to main content
Clean Architecture 70 XP · 7 min

The Details: Database, Web, and Frameworks

The database, web layer, and frameworks are delivery mechanisms — a good architect postpones these decisions and keeps business rules independent to avoid a costly technology marriage.

Showing
Ad (728×90)

Why this matters

Robert Martin's most confrontational claim: the database is a detail. Business rules don't care whether data is stored in MySQL, MongoDB, a flat file, or in memory. The Repository pattern abstracts this — the business logic calls repo.save(order) and the concrete implementation decides how. Swap MySQL for PostgreSQL by rewriting one class.

The web is a detail. HTTP is one delivery mechanism. The same Use Cases that serve a REST API could also serve a CLI, a message queue consumer, or a batch job. If your Use Case imports the web framework, it cannot be triggered from a message queue without rewriting it. A Use Case that accepts a plain DTO as input doesn't care who called it.

Frameworks are tools — don't marry them. The sign of a framework marriage: replacing the web framework requires rewriting business logic. A well-architected system lets you swap the framework by rewriting only a thin adapter layer. The business logic is untouched because it never knew the framework existed. You should be able to describe your entire business logic without mentioning a single framework, database, or protocol.

The problem

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.

The solution

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.

Key takeaway

You should be able to describe your entire business logic to a non-technical person without mentioning a single framework, database, or protocol. The database, web layer, and frameworks are delivery mechanisms — valuable tools, but details. Keep them at the edges. Business rules in the center, delivery mechanisms at the boundary.

Done with this lesson?

Mark it complete to earn XP and track your progress.