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
Early bird: €100 off with code EARLYBIRD100


🔒 Hello, this is Valentina with a premium issue of the Optivem Journal. I help Engineering Leaders & Senior Software Developers apply TDD in Legacy Code.


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