Optivem Journal

Optivem Journal

Clean Architecture

DDD + Clean Architecture: Where to Put Validation Logic

Should validation go in the API, application layer, or domain?

Valentina Jemuović's avatar
Valentina Jemuović
Apr 16, 2026
∙ Paid

“Where should validation go?”

You’ve seen:

  • validation in controllers

  • validation in services

  • validation duplicated in multiple places

And no one is really sure what’s “correct”.

So let’s make it concrete.

1. Validation at the API layer (input checks)

This is the first place data enters your system.

Here you check things like:

  • required fields exist

  • types are correct

  • format is valid (email, date, UUID, etc.)

Example:

  • email is missing → reject request

  • age is a string → reject request

  • orderDate is "not-a-date" → reject request

The API layer’s job is to fail fast with clear errors for malformed input, so the application and domain layers can work with clean, well-typed data.

2. Validation in the Application Layer (policies - use case rules)

This is where most real mistakes happen.

This layer handles rules like:

  • cannot create shipment if stock is insufficient

  • order cannot be placed if cart is empty

  • cannot refund payment if transaction is already settled beyond refund window

  • cannot process payment if currency is not supported for merchant

  • cannot cancel order after it has been shipped

  • cannot apply discount code if it is expired or not eligible

These are not input checks anymore.

They are policies. A policy is a rule about whether an action is allowed right now, given the current state of the world.

Every one of these needs something outside the aggregate itself to evaluate — inventory, the clock, a merchant config, a discount catalog. In DDD terms, they cross the aggregate boundary.

That’s why they don’t belong in the domain object. An Order alone doesn’t know whether stock exists. The application layer orchestrates: it fetches what’s needed, evaluates the policy, and either proceeds or rejects.

Example flow:

  • API sends a valid, well-formed request

  • application checks the relevant policies

  • if a policy fails → reject here, before touching the domain

This is where you stop things that are technically valid, but not allowed in this situation.


⚡ Register now: ATDD – Acceptance Testing Workshop
Get 100 EUR off with code EARLYBIRD100


3. Validation in the Domain Layer (invariants)

This is the strictest level.

These are rules that must never be broken, no matter where the code is called from.

An invariant is a rule that must always hold for a given object, no matter who calls it, from where, at what time.

Examples:

  • order must always have at least one line item

  • order total cannot be negative

  • payment cannot be “SUCCESSFUL” without a transaction reference

  • reserved quantity cannot exceed available stock

  • order status must follow valid transitions: CREATED → PAID → SHIPPED → DELIVERED

  • order cannot have SHIPPED status without a tracking number

If this is broken, your system is already inconsistent.

So this validation:

  • is enforced when creating or modifying core objects

  • is tied directly to the business model itself

The key principle: the domain never trusts its callers.

Even if the API already checked something, the domain re-checks any invariant. Why? Because the domain can be invoked from anywhere — a job, an event, a test, another bounded context. It cannot assume a well-behaved API sits in front of it.

What you should NOT do

This is where most real-world code goes wrong:

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