When “Clean Architecture” Isn’t So Clean: Rethinking Validation in the Domain

Published: (February 5, 2026 at 10:12 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

Some time ago, I was asked to maintain a Java application supposedly built around Clean Architecture. In reality… not so much.

Almost every entity fell into one of these categories:

  • Sprinkled with Jakarta annotations → framework leakage straight into the domain
  • Stuffed with Guava Preconditions.checkArgument() → intrusive utility dependencies
  • Polluted with ad‑hoc null checks → inconsistent, repetitive, and rarely tested

I eventually stumbled upon a constructor like this (anonymized, but painfully real):

public Invoice(String customerEmail, BigDecimal amount, List items, LocalDate dueDate) {
    this.customerEmail = Preconditions.checkNotNull(customerEmail, "email required");
    Preconditions.checkArgument(customerEmail.contains("@"), "bad email");
    Preconditions.checkArgument(customerEmail.length()  0, "amount positive");

    this.items = Preconditions.checkNotNull(items);
    Preconditions.checkArgument(!items.isEmpty(), "items empty");
    Preconditions.checkArgument(items.stream().allMatch(Objects::nonNull), "null item");

    this.dueDate = Preconditions.checkNotNull(dueDate);
    Preconditions.checkArgument(dueDate.isAfter(LocalDate.now()), "future date");
}

It worked, sure.
Worse: every failure collapsed into an IllegalArgumentException, making it impossible to distinguish bad user input from developer mistakes once it hit production.

Existing validation attempts

I initially experimented with the usual suspects:

  • Jakarta Bean Validation
  • Apache Commons Validate

Both came with trade‑offs I couldn’t accept:

  • Strong coupling to external frameworks
  • Annotation‑heavy APIs that don’t compose well for complex rules
  • Generic exceptions that flatten all validation failures into the same bucket

Bean Validation 3.0 + records improves things, but it’s still annotation‑driven—and business rules rarely fit nicely into declarative annotations.

A domain‑native validation approach

If I was going to introduce any dependency into the domain layer, it had to:

  • Have zero transitive dependencies (single JAR, java.base only)
  • Throw typed, meaningful exceptions (not IllegalArgumentException)
  • Support fluent, readable chaining
  • Allow custom predicates without turning everything into lambda soup

The same constructor now looks like this:

public Invoice(String email, BigDecimal amount, List items, LocalDate dueDate) {
    this.email = Assert.field("email", email)
                       .notBlank()
                       .email()
                       .maxLength(100)
                       .value();

    this.amount = Assert.field("amount", amount)
                        .positive()
                        .value();

    this.items = Assert.field("items", items)
                       .notEmpty()
                       .noNullElement()
                       .value();

    this.dueDate = Assert.field("dueDate", dueDate)
                         .inFuture()
                         .value();
}

Same rules.
The intent reads like business logic.

Instead of a generic IllegalArgumentException: bad email, you now get something like:

EmailFormatInvalidException {
  fieldName = "email",
  invalidValue = "...",
  constraint = "EMAIL_FORMAT"
}

Benefits

  • Clear monitoring – we can automatically distinguish between

    • User input errors → HTTP 400
    • Programming errors → HTTP 500

    No fragile string matching, no guessing.

  • Explicit custom predicates

    Assert.field("iban", iban)
          .notBlank()
          .satisfies(this::isValidIBANChecksum, "Checksum failed");
  • No annotations – works for teams that avoid Hibernate‑first patterns (@NotNull on JPA entities adds little value).

  • No AOP magic – intentional, constructor‑level defense.

The bigger picture

After this experience, I see validation libraries falling into two camps:

CampCharacteristics
Framework validation (Jakarta, Spring)Excellent at the HTTP/binding layer; awful for domain purity
Utility validation (Guava, Apache Commons)Reusable; weak on expressiveness and error semantics

There’s a missing middle ground: domain‑native validation that reads like business rules and fails with typed domain events. That’s the gap I was trying to fill.

Open questions for the community

  • How do you handle validation without contaminating your domain?
  • Do you accept Jakarta annotations in entities?
  • Do you write defensive code by hand?
  • Or have you found a different approach entirely?

And an honest question: do typed validation exceptions (e.g. NumberValueTooLowException) actually help you in production—or is this just over‑engineering?


Project:

Curious to hear how others approach this.

Back to Blog

Related posts

Read more »

Garbage Collection in Depth (JAVA)

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as we...