Skip to main content

Sign in to CleanKata

Track your progress, earn XP, and unlock every lesson.

By signing in you agree to our Terms of Use and Privacy Policy.

Clean Architecture70 XP7 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.

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.

🔧 Some exercises may still have errors. If something seems wrong, use the Feedback button (bottom-right of the page) to report it — it helps us fix it fast.

Hint: If your Use Case imports anything from the web framework or database ORM, it's no longer a Use Case — it's a controller.

✗ Your version

Architecture Layers: Entities and Use Cases — CleanKata — CleanKata