Optivem Journal

Optivem Journal

Clean Architecture

DDD: Stop Making Your Domain a Data Bag

Your domain needs more than getters

Valentina Jemuović's avatar
Valentina Jemuović
Sep 18, 2026
∙ Paid

ATDD in Legacy Code: catch regression bugs before production.
Hands-on work with your teams. Limited spaces for 2027.

Let's talk


📅 Join our next live course: ATDD, Clean Architecture, Pipelines
Ealy bird: €100 off with code EARLYBIRD100


Your SubscriptionService has 800 lines.

It activates subscriptions, handles cancellations, validates plan changes, checks expiration...

Meanwhile Subscription is just getters and setters.

That’s the problem.

❌Your Domain Is Just Data (Anemic Domain)

public class Subscription {
    private String customerId;
    private SubscriptionPlan plan;
    private SubscriptionStatus status;
    private LocalDate startedAt;
    private LocalDate expiresAt;
    private BigDecimal price;

    public String getCustomerId() {
        return customerId;
    }

    public void setCustomerId(String customerId) {
        this.customerId = customerId;
    }

    public SubscriptionPlan getPlan() { ... }

    public void setPlan(SubscriptionPlan plan) { ... }

    public SubscriptionStatus getStatus() { ... }

    public void setStatus(SubscriptionStatus status) { ... }

    public LocalDate getStartedAt() { ... }

    public void setStartedAt(LocalDate startedAt) { ... }

    public LocalDate getExpiresAt() { ... }

    public void setExpiresAt(LocalDate expiresAt) { ... }

    public BigDecimal getPrice() { ... }

    public void setPrice(BigDecimal price) { ... }
}

There isn’t much here.

The Subscription holds data.

The business rules live somewhere else:

public class SubscriptionService {

    public void cancel(Subscription subscription) {
        if (subscription.getStartedAt().plusDays(7).isBefore(LocalDate.now())) {
            throw new IllegalStateException(
                "Free cancellation is only available during the 7-day trial"
            );
        }

        subscription.setStatus(SubscriptionStatus.CANCELLED);
    }

    public void changePlan(
        Subscription subscription,
        SubscriptionPlan newPlan
    ) {
        if (newPlan == subscription.getPlan()) {
            throw new IllegalArgumentException(
                "Subscription is already on this plan"
            );
        }

        subscription.setPlan(newPlan);
        subscription.setPrice(newPlan.getPrice());
    }

    public void renew(Subscription subscription) {
        if (subscription.getStatus() != SubscriptionStatus.ACTIVE) {
            throw new IllegalStateException(
                "Only active subscriptions can be renewed"
            );
        }

        subscription.setExpiresAt(
            subscription.getExpiresAt().plusMonths(1)
        );
    }
}

At first, this doesn’t look terrible.

But keep adding business rules.

Soon you have:

SubscriptionService
├── startTrial()
├── cancel()
├── changePlan()
├── renew()
├── pause()
├── resume()
├── extendTrial()
├── changeBillingCycle()
├── updatePaymentMethod()
└── ...

And your Subscription is still just data.

That’s where the real problems appear:

  • Rules get scattered across services

  • The same rules get duplicated in different places

  • Rules can be bypassed by calling setters directly

  • Changing a business rule means hunting through services to find everywhere it is implemented

  • The Subscription object can’t protect its own state

✅Your Domain Owns the Rules (Rich Domain)

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2026 Valentina Jemuović, Optivem · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture