DDD + Clean Architecture: Where to Put Validation Logic
Should validation go in the API, application layer, or domain?
“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:
emailis missing → reject requestageis a string → reject requestorderDateis"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 → DELIVEREDorder cannot have
SHIPPEDstatus 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:

