CtrlK
BlogDocsLog inGet started
Tessl Logo

pantheon-ai/design-principles

Strategic architecture, tactical design, and testable code principles (SOLID, Clean Architecture, Design Patterns, Testable Design)

97

Quality

97%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

entity-rich-not-anemic.mdclean-architecture/references/

title:
Build Rich Domain Models Not Anemic Data Structures
impact:
HIGH
impactDescription:
centralizes business logic, prevents scattered rules
tags:
entity, domain-model, anemic, behavior

Build Rich Domain Models Not Anemic Data Structures

Entities should contain behavior, not just data. Anemic domain models push business logic into services, scattering rules and duplicating validation.

Incorrect (anemic domain model):

// domain/entities/Subscription.java
public class Subscription {
    private String id;
    private String planId;
    private LocalDate startDate;
    private LocalDate endDate;
    private String status;

    // Only getters and setters - no behavior
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
    public LocalDate getEndDate() { return endDate; }
    public void setEndDate(LocalDate endDate) { this.endDate = endDate; }
}

// application/services/SubscriptionService.java
public class SubscriptionService {
    public void cancel(Subscription sub) {
        if (!sub.getStatus().equals("active")) {
            throw new IllegalStateException("Cannot cancel");
        }
        sub.setStatus("cancelled");
        sub.setEndDate(LocalDate.now());
    }

    public void renew(Subscription sub, int months) {
        if (!sub.getStatus().equals("active")) {
            throw new IllegalStateException("Cannot renew");
        }
        sub.setEndDate(sub.getEndDate().plusMonths(months));
    }
    // Logic scattered across many services
}

Correct (rich domain model):

// domain/entities/Subscription.java
public class Subscription {
    private final SubscriptionId id;
    private final PlanId planId;
    private final LocalDate startDate;
    private LocalDate endDate;
    private SubscriptionStatus status;

    public void cancel() {
        ensureActive();
        this.status = SubscriptionStatus.CANCELLED;
        this.endDate = LocalDate.now();
    }

    public void renew(Period extension) {
        ensureActive();
        if (extension.getMonths() < 1) {
            throw new InvalidExtensionPeriodException(extension);
        }
        this.endDate = this.endDate.plus(extension);
    }

    public boolean isExpired() {
        return LocalDate.now().isAfter(this.endDate);
    }

    public boolean canUpgradeTo(Plan newPlan) {
        return this.status == SubscriptionStatus.ACTIVE
            && newPlan.isUpgradeFrom(this.planId);
    }

    private void ensureActive() {
        if (this.status != SubscriptionStatus.ACTIVE) {
            throw new InactiveSubscriptionException(this.id);
        }
    }
}

// application/usecases/CancelSubscriptionUseCase.java
public class CancelSubscriptionUseCase {
    public void execute(SubscriptionId id) {
        Subscription sub = repository.findById(id);
        sub.cancel();  // Business logic in entity
        repository.save(sub);
    }
}

Benefits:

  • Business rules live with the data they operate on
  • Impossible to forget validation when manipulating data
  • Entity documents its own capabilities and constraints

Reference: Anemic Domain Model Anti-pattern

clean-architecture

SKILL.md

tile.json