Skip to main content
Clean Architecture 70 XP · 7 min

Business Rules and Entities

Critical Business Rules would exist even without a computer — Entities encapsulate these rules and data, forming the most stable, UI-and-persistence-agnostic heart of the system.

Showing
Ad (728×90)

Why this matters

There are two kinds of business rules. Critical Business Rules are the rules that would exist even if the software didn't — a bank calculates loan interest whether the system runs on paper ledgers or cloud servers. These rules are the most valuable thing in the system. They must be isolated, protected, and untouched by anything else. Application Business Rules are use-case-specific rules that only exist because there is software — validation workflows, input transformation steps, orchestration logic.

Entities are the objects that embody Critical Business Rules. An Entity is a pure business concept: a Loan, an Order, an Invoice. It contains data and the rules that operate on that data. It knows nothing about databases, UIs, frameworks, or HTTP. It can be instantiated in a Python test file with no imports beyond the standard library. If an Entity imports SQLAlchemy, it has been corrupted. The framework has invaded the most stable, valuable part of the system.

The problem

A Loan class that has calculate_interest() but also save_to_db(), to_html(), and send_email_notification() — the critical business rule is buried with technical concerns it should never know about.

Bad

from sqlalchemy import Column, Float, Integer
from sqlalchemy.ext.declarative import declarative_base
import smtplib
from decimal import Decimal

Base = declarative_base()

class Loan(Base):
    __tablename__ = "loans"
    id        = Column(Integer, primary_key=True)
    principal = Column(Float)
    rate      = Column(Float)
    term      = Column(Integer)

    def calculate_interest(self) -> float:
        # Critical business rule — would exist on a paper ledger too
        return self.principal * self.rate * self.term

    def save_to_db(self, session):
        session.add(self)
        session.commit()

    def to_html(self) -> str:
        interest = self.calculate_interest()
        return f"<p>Loan {self.id}: interest = {interest:.2f}</p>"

    def send_email_notification(self, recipient: str):
        smtp = smtplib.SMTP("localhost")
        smtp.sendmail("bank@co.com", recipient,
                      f"Interest: {self.calculate_interest():.2f}")

# The critical rule is correct but corrupted.
# To test calculate_interest: SQLAlchemy, DB session, and smtplib must exist.
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import nodemailer from "nodemailer";

@Entity()
export class Loan {
  @PrimaryGeneratedColumn() id!: number;
  @Column("float") principal!: number;
  @Column("float") rate!: number;
  @Column("int")   term!: number;

  calculateInterest(): number {
    return this.principal * this.rate * this.term;
  }

  toHtml(): string {
    return `<p>Loan ${this.id}: interest = ${this.calculateInterest().toFixed(2)}</p>`;
  }

  async sendEmailNotification(recipient: string): Promise {
    const t = nodemailer.createTransport({ host: "localhost", port: 25 });
    await t.sendMail({ from: "bank@co.com", to: recipient,
      subject: "Loan interest", text: `${this.calculateInterest()}` });
  }
}
// To test calculateInterest: TypeORM decorators require DB setup.
// The Entity has been corrupted by three technical concerns.

The solution

A pure Loan entity with only the critical business rules — no ORM, no HTML, no email. Persistence, presentation, and notification are separate concerns handled by separate classes.

Good

from decimal import Decimal
from dataclasses import dataclass

@dataclass
class Loan:
    principal: Decimal
    rate: Decimal   # annual interest rate, e.g. Decimal("0.05") for 5%
    term: int       # years

    def calculate_interest(self) -> Decimal:
        """Critical Business Rule — would be calculated on paper without software."""
        return self.principal * self.rate * self.term

    def monthly_payment(self) -> Decimal:
        """Another critical rule — depends only on principal, rate, and term."""
        monthly_rate = self.rate / 12
        n = self.term * 12
        if monthly_rate == 0:
            return self.principal / n
        return self.principal * monthly_rate / (1 - (1 + monthly_rate) ** -n)

# Test with plain Python — zero infrastructure:
# loan = Loan(Decimal("10000"), Decimal("0.05"), 2)
# assert loan.calculate_interest() == Decimal("1000.00")

# Technical concerns are separate, isolated classes:
# class SqlLoanRepository(LoanRepository): ...  — persistence
# class LoanPresenter: def to_html(loan: Loan) -> str: ...  — presentation
# class LoanNotifier: async def send(loan: Loan, email: str): ...  — notification
# The Entity knows none of them exist.
// Pure domain entity — no imports from any framework or library
export class Loan {
  constructor(
    readonly principal: number,
    readonly rate: number,  // annual interest rate, e.g. 0.05 for 5%
    readonly term: number,  // years
  ) {}

  calculateInterest(): number {
    // Critical Business Rule — exists in banking regardless of software
    return this.principal * this.rate * this.term;
  }

  monthlyPayment(): number {
    const monthlyRate = this.rate / 12;
    const n           = this.term * 12;
    if (monthlyRate === 0) return this.principal / n;
    return this.principal * monthlyRate / (1 - Math.pow(1 + monthlyRate, -n));
  }
}

// Test with zero dependencies:
// const loan = new Loan(10_000, 0.05, 2);
// expect(loan.calculateInterest()).toBe(1000);

// Technical concerns live elsewhere:
// class TypeOrmLoanRepository implements LoanRepository { ... }  — persistence
// class LoanPresenter { toHtml(loan: Loan): string { ... } }     — presentation
// class LoanNotifier { async send(loan: Loan, email: string) }   — notification
// The Entity imports nothing. Framework upgrades cannot corrupt it.

Key takeaway

An Entity is a business concept that exists regardless of technology. If it imports a framework, it has been corrupted. The Entity is the innermost layer of the system — it must know nothing about databases, UIs, or external services. Everything else depends on it. It depends on nothing.

Done with this lesson?

Mark it complete to earn XP and track your progress.