Why this matters
Entities encapsulate enterprise-wide critical business rules. A loan's interest calculation formula, the rule that an order cannot be cancelled after shipping, the rule that a patient cannot have two appointments at the same time — these exist in the real world regardless of software. Entities are the most stable, reusable objects in the system.
Use Cases encapsulate application-specific business rules. They orchestrate the flow of data to and from Entities to achieve the goal of the use case. "Apply for a Loan" is a Use Case: it validates the input, checks credit score, calls the Loan Entity to compute interest, and persists the result. Use Cases know about Entities — they do not know about HTTP, SQL, or any UI framework.
The test for correctness: if your Use Case imports anything from the web framework or database ORM, it is no longer a Use Case — it is a controller. A real Use Case can be exercised from a unit test with no running server and no database connection.
The problem
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 });
}
The solution
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.
Key takeaway
Entities hold the rules that exist in the real world. Use Cases orchestrate those rules for a specific application scenario. Neither layer knows about HTTP, SQL, or any framework. If your Use Case imports a web framework, it has been promoted to controller — and your architecture has lost its most important separation.