Skip to main content
Clean Architecture 70 XP · 7 min

Architecture Layers: Entities and Use Cases

Entities hold critical business rules that exist across the enterprise; Use Cases orchestrate application-specific flows and are isolated from UI and database changes.

Showing
Ad (728×90)

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.

Done with this lesson?

Mark it complete to earn XP and track your progress.