Building REST APIs

Request Validation

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

আমরা এখন পর্যন্ত request DTO design করেছি।

For example:

public record CreateProductRequest(
        String name,
        BigDecimal price
) {
}
public record SetInventoryRequest(
        int availableQuantity
) {
}
public record CreateOrderRequest(
        List<CreateOrderItemRequest> items
) {
}
public record CreateOrderItemRequest(
        String productId,
        int quantity
) {
}

কিন্তু এই DTOs এখন invalid input-ও represent করতে পারে।

For example:

{
  "name": "",
  "price": -10
}

অথবা:

{
  "items": []
}

অথবা:

{
  "items": [
    {
      "productId": "",
      "quantity": 0
    }
  ]
}

এই ধরনের input যত তাড়াতাড়ি সম্ভব HTTP boundary-তে reject করা useful।

Spring Boot application-এ আমরা সাধারণত Jakarta Bean Validation ব্যবহার করতে পারি।

এই lesson-এর goal:

Transport-level validation early perform করা, while keeping business invariants and cross-domain validation in their correct owners.


Validation Is Not One Thing

Backend codebase-এ "validation" শব্দটি অনেক broad।

সব validation একই layer-এ থাকা উচিত নয়।

আমাদের system-এ broadly চারটি boundary আছে:

Transport validation

Application / UseCase validation

Domain validation

Database integrity

Each has a different responsibility।


Transport Validation

Transport validation answers questions such as:

Required field supplied হয়েছে?

String blank কি না?

Collection empty কি না?

Number basic range satisfy করছে?

Request shape valid কি না?

Example:

CreateOrderItemRequest.quantity > 0

This is appropriate to reject at the HTTP boundary।


Application Validation

Application validation requires application state or multiple concepts।

Examples:

Product exists?

Product active?

Same Product requested twice?

Order belongs to current customer?

Inventory sufficient?

These generally require:

Repositories

authenticated identity

multiple domain objects

So UseCase coordinates them।


Domain Validation

Domain validation protects intrinsic object invariants।

Examples:

Product price cannot be negative

Inventory quantity cannot be negative

OrderItem quantity must be positive

Order cannot be empty

PAID Order cannot be cancelled

These should remain protected even if request validation already exists।


Database Integrity

PostgreSQL later reinforces structural integrity।

Examples:

NOT NULL

foreign keys

CHECK constraints

unique constraints

Database is the last persistence guard, not the primary HTTP validation system।


Bean Validation

Jakarta Bean Validation lets us declare request constraints directly on DTO fields/components।

Examples include:

@NotNull

@NotBlank

@NotEmpty

@Positive

@PositiveOrZero

@Size

@Valid

These annotations describe transport/input expectations concisely।


@Valid

In a Spring MVC Handler:

@PostMapping
public ResponseEntity<ProductResponse> create(
        @Valid
        @RequestBody
        CreateProductRequest request
) {
    ...
}

@Valid tells Spring to validate the deserialized request object before entering normal Handler logic।

If validation fails:

UseCase is not invoked

and the request should become a client error, typically:

400 Bad Request

through centralized error handling।


CreateProductRequest Validation

Our Product requirements:

name required

price required

price cannot be negative

A request DTO can express basic versions of these rules:

public record CreateProductRequest(

        @NotBlank
        String name,

        @NotNull
        @PositiveOrZero
        BigDecimal price

) {
}

This rejects obvious invalid HTTP input before calling the UseCase।


Why @PositiveOrZero?

Our accepted Product rule is:

price >= 0

We deliberately did not define:

price > 0

So:

@Positive

would accidentally invent a stricter business rule।

Correct transport constraint:

@PositiveOrZero

matches the accepted invariant।


Validation Annotation Must Match the Business Rule

This is important।

Do not choose constraints because they "sound right."

For example:

@Min(1)

on Product price would silently make free Products impossible।

No requirement says that।

Validation code is still product behaviour at the boundary।

It must reflect actual requirements।


Why Domain Still Validates Price

Even with:

@PositiveOrZero

we still keep:

product.changePrice(...)

and Product construction guarding negative price।

Why?

Because Product may be created or changed from code paths other than this DTO।

The guarantees differ:

Bean Validation
→ HTTP request is acceptable
Product invariant
→ Product can never intentionally hold invalid price

Both are useful।


Do Not Treat This as Wasteful Duplication

Consider:

price cannot be negative

Transport layer protects external input quality।

Domain protects internal correctness।

If the same concept appears at both boundaries, that can be correct because each boundary guarantees something different।


Product Name

Request:

@NotBlank
String name

rejects:

null

""

"   "

which fits our Product invariant that name must be meaningful।

Domain still protects the same invariant。


Should We Add Name Length Limits?

We could add:

@Size(max = 200)

But do we have a Product requirement saying:

Product name maximum 200 characters

?

No।

Therefore don't invent one yet।

Later database/schema/API constraints may require a documented maximum, but it should be an intentional decision।


SetInventoryRequest

Our admin operation:

set available quantity

with invariant:

availableQuantity >= 0

A request DTO:

public record SetInventoryRequest(

        @PositiveOrZero
        int availableQuantity

) {
}

This looks reasonable, but Java primitive types introduce a nuance।


Primitive Defaults and Missing Fields

Suppose JSON:

{}

is deserialized into:

int availableQuantity

Primitive int defaults to:

0

Now the application may not distinguish:

field omitted

from:

client explicitly sent 0

But for a PUT request where availableQuantity is required, we likely want omission to be invalid।


Use Wrapper Types When Presence Matters

Better request DTO:

public record SetInventoryRequest(

        @NotNull
        @PositiveOrZero
        Integer availableQuantity

) {
}

Now:

{}

produces:

null

and @NotNull can reject the missing field।

While:

{
  "availableQuantity": 0
}

is valid।

This is an important DTO design technique।


Transport Type Does Not Need to Match Domain Type Exactly

Request DTO may use:

Integer

to represent:

presence vs absence

while domain Inventory uses:

int

because once valid input reaches the domain, quantity is guaranteed to exist।

Transport requirements and domain requirements are different।


CreateOrderRequest

Current request:

public record CreateOrderRequest(
        List<CreateOrderItemRequest> items
) {
}

Accepted rules include:

at least one item

each item has Product ID

quantity positive

We can express the basic request shape using Bean Validation।


Validate the Collection

Conceptually:

public record CreateOrderRequest(

        @NotEmpty
        List<@Valid CreateOrderItemRequest> items

) {
}

But for nested object validation, a more conventional declaration is:

public record CreateOrderRequest(

        @NotEmpty
        List<CreateOrderItemRequest> items

) {
}

with:

@Valid

applied to the collection component:

public record CreateOrderRequest(

        @NotEmpty
        @Valid
        List<CreateOrderItemRequest> items

) {
}

This tells the validator:

validate the list

and recursively validate each item

CreateOrderItemRequest

A transport model:

public record CreateOrderItemRequest(

        @NotBlank
        String productId,

        @NotNull
        @Positive
        Integer quantity

) {
}

Why Integer rather than int?

Again, because:

{
  "productId": "P-100"
}

should be distinguishable from:

{
  "productId": "P-100",
  "quantity": 0
}

Both are invalid, but for different validation reasons।


@Positive

Here @Positive is appropriate because our accepted Order rule says:

quantity must be a positive whole number

So:

0

and:

-1

are invalid।


Whole Number Is Already Expressed by the Type

Using:

Integer quantity

means JSON is expected to represent an integer-compatible value for normal deserialization।

We do not need a custom:

isWholeNumber

business validator when the transport type itself already expresses it।


Nested Validation Matters

Suppose request:

{
  "items": [
    {
      "productId": "",
      "quantity": -2
    }
  ]
}

If we validate only:

@NotEmpty
List<CreateOrderItemRequest> items

the list itself is non-empty։

Without nested validation, invalid item fields may not be checked by the DTO validator։

That's why:

@Valid

on nested objects/collections matters।


A Complete CreateOrderRequest

Conceptually:

public record CreateOrderRequest(

        @NotEmpty
        @Valid
        List<CreateOrderItemRequest> items

) {
}

and:

public record CreateOrderItemRequest(

        @NotBlank
        String productId,

        @NotNull
        @Positive
        Integer quantity

) {
}

This gives us a strong transport-level baseline।


Is Duplicate Product Validation a Bean Constraint?

We could write a custom annotation:

@UniqueProductIds

on CreateOrderRequest

But should we?

Probably not yet।

Our accepted architecture treats duplicate Product IDs as part of Create Order application/business input rules।

A simple check inside:

CreateOrderUseCase

is clearer and avoids premature custom validation infrastructure।


Why Keep Duplicate Validation in UseCase?

Duplicate Product detection requires understanding:

these item identifiers represent Product references

It is more than a generic JSON shape check।

It also should remain true if Create Order is invoked from another transport later։

So:

Handler
→ validates each item shape
CreateOrderUseCase
→ rejects duplicate Product references

is a good split।


Order Can Still Protect Duplicates Too

Remember from domain modeling:

valid Order should not contain duplicate Products

So Order construction can also protect this invariant।

Then:

UseCase
→ rejects early before side effects
Order
→ protects final domain validity

This is deliberate layered protection।


Product Existence Is Not Bean Validation

Avoid creating:

@ExistingProduct
String productId;

if the validator calls:

ProductRepository

during request validation।

That creates problematic coupling:

HTTP validation
→ persistence access

It also duplicates the Product lookup that CreateOrderUseCase needs anyway।


Product Existence Belongs in UseCase

Correct:

CreateOrderUseCase
    ↓
ProductRepository.findById(...)

If missing:

application failure

later mapped to:

404 Not Found

Transport validator should not query the database just to answer this।


Product Active State Is Not Request Validation

Likewise, don't write:

@OrderableProduct
String productId;

if it needs live Product state।

Whether Product is active depends on current application state।

That belongs to the actual Create Order workflow।


Inventory Availability Is Definitely Not DTO Validation

Avoid a custom annotation such as:

@SufficientInventory
CreateOrderItemRequest item;

that queries Inventory।

Why?

Because availability must be evaluated:

inside the actual business operation

with correct transaction/concurrency semantics

A request validator querying Inventory earlier can become stale before the operation runs।


Check-Then-Act Risk

Imagine validation does:

Inventory = 5
request quantity = 5
→ validator passes

Then another request consumes those units։

CreateOrderUseCase now runs。

Inventory may be:

0

The earlier validation result no longer matters।

This demonstrates why live business state checks belong inside the operation that acts on the state।


Validation Should Not Perform Side Effects

Request validation should not:

reserve Inventory

decrease Inventory

create Product

write database state

Validation at the transport boundary should be side-effect free।

Business mutation happens only once the UseCase executes।


Path Variable Validation

Suppose:

GET /orders/{orderId}

with:

orderId = ""

normally URI routing itself makes an empty path segment unlikely।

But identifier parsing may still fail if an ID has a specific representation।

For example, once ID strategy is defined, converting:

invalid string

into OrderId may fail।

That is a request-level error and can map to:

400 Bad Request

Do Not Invent ID Format Validation Yet

We have intentionally deferred exact ID representation։

So don't add:

UUID regex

prefix regex

numeric-only rule

to DTOs before our ID strategy is chosen।

Transport validation must reflect actual contracts, not hypothetical future formats।


Query Parameter Validation

Later pagination may accept:

page

size

Those inputs will need validation such as:

page >= 0

size within server-defined bounds

But pagination has a dedicated lesson।

Don't mix those decisions into current request models prematurely।


Handler Activation With @Valid

Example:

@PostMapping
public ResponseEntity<ProductResponse> create(
        @Valid
        @RequestBody
        CreateProductRequest request
) {
    ...
}

For Order:

@PostMapping
public ResponseEntity<OrderResponse> create(
        @Valid
        @RequestBody
        CreateOrderRequest request
) {
    ...
}

For Inventory:

@PutMapping("/{productId}")
public InventoryResponse set(
        @PathVariable String productId,
        @Valid
        @RequestBody
        SetInventoryRequest request
) {
    ...
}

This is the main Handler-level pattern।


What Happens When Validation Fails?

Conceptually:

HTTP Request
    ↓
JSON deserialization
    ↓
Bean Validation
    ↓
invalid?
    ├── yes → error handling → 400
    └── no  → Handler → UseCase

The UseCase should not run if transport input already violates the request contract।


Validation Error Response

We eventually want a consistent structured error rather than raw framework output।

For example conceptually:

{
  "code": "VALIDATION_FAILED",
  "message": "Request validation failed",
  "fields": [
    {
      "field": "items[0].quantity",
      "message": "must be greater than 0"
    }
  ]
}

Exact response format comes in the consistent API error lesson।

The important point now:

validation failures
→ centralized API error handling

not ad hoc response construction in every Controller।


Don't Expose Framework Exception Names

Bad client response:

{
  "error": "MethodArgumentNotValidException"
}

Clients should understand API meaning, not Spring internals।

Map framework failures into stable application/API error representation।


Custom Validation Messages

Annotations can provide custom messages:

@NotBlank(
        message = "Product name is required"
)
String name

This can improve immediate API messages։

But avoid turning every DTO into a long collection of repeated prose if centralized conventions provide enough clarity।


Stable Error Codes Matter More Than Human Text

Human-readable validation message might change:

"must be greater than 0"

to:

"quantity must be positive"

Clients should not parse those strings programmatically।

Stable machine-readable:

VALIDATION_FAILED

plus field information is more robust।


@NotNull vs @NotEmpty vs @NotBlank

These are easy to confuse।

@NotNull

Rejects:

null

Useful for values whose presence matters։


@NotEmpty

Useful for collections/strings where:

null

and empty should be rejected।

For Order items:

@NotEmpty
List<...> items

is appropriate։


@NotBlank

Useful for text where:

null

empty

whitespace-only

should be rejected।

For:

Product name

Product ID string

this often fits।


Number Constraints

Relevant:

@Positive
→ > 0

@PositiveOrZero
→ >= 0

Our domain maps naturally:

Order quantity
→ @Positive
Inventory available quantity
→ @PositiveOrZero
Product price
→ @PositiveOrZero

This alignment keeps request validation consistent with domain rules।


@Min and @Max

Could be useful for business rules such as:

quantity <= 100

But we do not currently have such a requirement।

Do not add arbitrary maximum order quantity।

Earlier course planning explicitly treated such values as illustrative, not canonical।


Product Update Validation Is Different From Create

For create:

name required

price required

For PATCH update:

name may be absent

price may be absent

So we cannot simply reuse:

CreateProductRequest

for update।

This is another reason operation-specific DTOs matter।


UpdateProductRequest

A pragmatic current model:

public record UpdateProductRequest(
        String name,
        BigDecimal price
) {
}

But applying:

@NotBlank
String name

would make name required even when updating only price։

That conflicts with PATCH semantics।


Optional Field Validation

What we want conceptually:

if name is absent
→ okay

if name is supplied
→ must not be blank

if price is absent
→ okay

if price is supplied
→ must be >= 0

Some constraints naturally ignore null

For example numeric constraints often allow null unless paired with @NotNull

So:

public record UpdateProductRequest(

        String name,

        @PositiveOrZero
        BigDecimal price

) {
}

still leaves the question of non-blank optional name।


Optional String Validation Needs Care

@NotBlank rejects null, which would make omission invalid।

One option is to interpret PATCH using a different request structure or custom validation.

But for our current course, we should not overcomplicate the DTO before update semantics are implemented concretely।

A simple application-level rule can be:

name == null
→ unchanged

name != null
→ Product.rename(name)

and Product.rename() rejects blank names।

This lets domain protection handle invalid supplied non-null name।


Is That Acceptable?

Yes, because:

blank name

still cannot reach valid Product state।

The API may return a mapped 400 from the resulting application/domain validation failure।

If we later want field-level transport validation for optional strings, we can introduce a targeted constraint once the PATCH contract is finalized।

Don't create infrastructure before the problem requires it।


PATCH With No Changes

What if client sends:

{}

?

Should that be:

valid no-op

or:

invalid because at least one field must change

Current requirements don't specify।

We should not invent that behaviour in this lesson।

When implementing Product update, this can be decided explicitly։


Do Not Use Bean Validation for Authorization

Avoid annotations like:

@Admin
CreateProductRequest request

inside DTO validation।

Authorization asks:

who is this caller?

what are they allowed to do?

Spring Security/application security boundary owns that।

Request validation only checks request input contract։


Do Not Use Bean Validation for Ownership

Similarly:

@OwnedOrder
String orderId

would likely need authenticated context + Repository。

Ownership belongs in:

UseCase / security-aware application flow

not a generic Bean Validator।


Do Not Use Bean Validation for Payment Eligibility

Avoid:

@PayableOrder

on path/request data।

Payment eligibility depends on current Order state and possibly provider/integration state।

That belongs in:

PayOrderUseCase

and domain behaviour।


Validation Should Be Cheap

Transport validation should generally involve:

local request values

not:

network calls

database calls

external Service calls

Why?

Because it keeps validation:

fast

predictable

side-effect free

easy to reason about

Live state belongs in the actual operation।


Bean Validation Does Not Replace Parsing Errors

Suppose client sends:

{
  "price": "not-a-number"
}

Spring/Jackson may fail deserialization before Bean Validation even runs।

This is still:

invalid request

and should map to:

400 Bad Request

through centralized transport error handling։


Request Processing Pipeline

A useful mental model:

HTTP bytes
    ↓
JSON parsing
    ↓
DTO construction
    ↓
Bean Validation
    ↓
Handler mapping
    ↓
UseCase validation/coordination
    ↓
Domain invariants
    ↓
Repository / DB integrity

Each stage rejects problems appropriate to its responsibility।


Example: Invalid JSON

Request:

{
  "quantity":

fails at:

JSON parsing

No DTO exists yet।

No UseCase runs।

Result:

400 Bad Request

Example: Missing Required Field

Request:

{
  "name": "Keyboard"
}

for Product creation।

DTO:

price = null

Bean Validation:

@NotNull

fails।

UseCase does not run।

Result:

400 Bad Request

Example: Product Does Not Exist

Request:

{
  "items": [
    {
      "productId": "P-999",
      "quantity": 2
    }
  ]
}

DTO validation passes:

productId non-blank

quantity positive

Then:

CreateOrderUseCase

loads Product।

Repository returns no Product।

Application failure maps to:

404 Not Found

Correct boundary।


Example: Product Is Inactive

Same valid request।

Product exists but:

active = false

This is not DTO invalidity।

CreateOrderUseCase observes current Product state and rejects the workflow।

Maps to:

409 Conflict

Example: Insufficient Inventory

Request is perfectly valid:

quantity = 5

Inventory currently:

2

Domain/application operation rejects:

inventory.decrease(5)

or UseCase coordinates equivalent failure।

Maps to:

409 Conflict

Again, not transport validation।


Example: Negative Inventory Admin Input

Request:

{
  "availableQuantity": -5
}

Bean Validation rejects early:

400 Bad Request

If some internal caller later invokes:

inventory.setAvailableQuantity(-5);

domain also rejects।

Two different guarantees।


Example: Paid Order Cancellation

Request:

POST /orders/O-1001/cancel

has no invalid body।

Path is valid।

Authentication valid।

Order exists and belongs to caller।

But Order is:

PAID

Domain:

order.cancel();

rejects।

Mapped to:

409 Conflict

This is business-state validation, not request validation।


Avoid Preloading Domain Objects in Controller "Validation"

Bad:

@PostMapping("/orders/{id}/cancel")
public ... cancel(
        @PathVariable String id
) {
    Order order =
            repository.findById(...);

    if (order.status() != UNPAID) {
        return conflict();
    }

    return useCase.execute(...);
}

Now Handler is:

loading application state

duplicating lifecycle rules

It also creates possible state changes between pre-check and UseCase execution։

Let UseCase/domain own the operation।


Custom Validators: When Are They Useful?

Custom Bean Validation constraints can be useful for reusable local rules।

For example, if we had an explicitly defined application format:

SKU must match a fixed syntax

used across multiple request models, a custom validator could be reasonable।

But custom validation should not become a disguised Repository/Service layer।


Avoid Regex Before the Contract Exists

For ProductId:

@Pattern(...)
String productId

would only be appropriate after Product ID format is an intentional public contract।

Our ID strategy is deferred।

So current:

@NotBlank
String productId

is enough at transport level։


Validation Error Field Paths

Nested validation can identify fields conceptually like:

items[0].productId

items[0].quantity

items[2].quantity

This is useful for client UX because a frontend can associate errors with specific request fields।

Our centralized error response should preserve useful field paths later।


Do Not Return Domain Error Messages as Field Errors Automatically

Suppose Product domain throws:

Product price cannot be negative

That may map nicely to a field error in some cases।

But domain failures are not always tied to one request field।

For example:

Order cannot be cancelled

is an operation-level error।

Keep field-level validation and business errors conceptually distinct in the API response model।


Validation Order

You generally want inexpensive input checks before more expensive workflow work։

Conceptually:

parse
    ↓
validate request shape
    ↓
authenticate/authorize as appropriate
    ↓
execute UseCase

Actual security filter ordering is framework-controlled, but the principle remains:

don't hit database for obviously malformed input

where the framework can reject it first।


Security Can Run Before Controller Validation

In Spring applications, security filters may reject:

unauthenticated request

before request body validation reaches the Controller։

That's fine।

For protected endpoints, an unauthenticated caller may receive:

401

even if the JSON body is also invalid।

Security and validation are separate boundaries।


Avoid Depending on Validation Order for Business Correctness

Whether:

authentication

or:

body validation

happens first at framework level should not determine domain correctness।

The UseCase/domain must still protect business behaviour once invoked।


Validation and Tests

We should test transport validation at the HTTP/Handler level।

Examples:

empty Product name → 400

negative Product price → 400

missing Inventory quantity → 400

empty Order items → 400

zero Order quantity → 400

These tests verify Spring configuration and API contract।


Domain Tests Still Remain Separate

Example:

@Test
void productRejectsNegativePrice() {
    ...
}

Even if Controller validation also rejects negative price।

Why?

Because domain test protects Product independently of Spring MVC।


UseCase Tests Cover Live-State Rules

Examples:

missing Product

inactive Product

insufficient Inventory

duplicate Product IDs

ownership failure

These should not require HTTP to test։


Don't Test Annotation Presence Instead of Behaviour

Weak test:

reflect on CreateProductRequest
and assert @NotBlank exists

Better:

send invalid request
and verify API rejects it

Test the observable contract rather than implementation detail where practical।


A Practical DTO Set

At this stage, our transport DTOs can move toward:

public record CreateProductRequest(

        @NotBlank
        String name,

        @NotNull
        @PositiveOrZero
        BigDecimal price

) {
}
public record SetInventoryRequest(

        @NotNull
        @PositiveOrZero
        Integer availableQuantity

) {
}
public record CreateOrderRequest(

        @NotEmpty
        @Valid
        List<CreateOrderItemRequest> items

) {
}
public record CreateOrderItemRequest(

        @NotBlank
        String productId,

        @NotNull
        @Positive
        Integer quantity

) {
}

These cover confirmed basic input rules without database-backed validators or speculative limits।


Product Update Remains Deliberately Simpler

For PATCH:

public record UpdateProductRequest(
        String name,

        @PositiveOrZero
        BigDecimal price
) {
}

with semantics:

null field
→ not updated

and Product domain validation protects non-null supplied name։

This is a pragmatic interim contract।

If PATCH semantics later require explicit null handling, we can model it more precisely।


Bean Validation Dependency

When we implement this ticket in Spring Boot, the project will need the appropriate validation support in its dependency configuration।

We should add that dependency when the request-validation ticket actually requires it—

not during bootstrap when no validation existed։

This follows our project principle:

Add dependencies when a concrete feature needs them.


Don't Add Validation to Domain via Jakarta Annotations by Default

Could we write:

public class Product {

    @PositiveOrZero
    private BigDecimal price;
}

?

We could, but then Product validity depends partly on whether some validator happens to run।

Our domain methods already enforce invariants directly।

For core domain state, explicit Java guards are clearer and framework-independent।

Use Jakarta Bean Validation primarily at transport boundaries in this course।


Domain Should Remain Valid Without Spring

A Product unit test should work with:

new Product(...)

and get invariant protection immediately।

It should not require:

Validator

ApplicationContext

Spring proxy

to make Product valid।

This keeps domain behaviour plain and reliable।


Validation Is About Trust Boundaries

Think of input moving inward:

Raw JSON
→ untrusted
Validated Request DTO
→ transport contract satisfied
UseCase
→ application state checked
Domain objects
→ intrinsic invariants satisfied
Database
→ persisted integrity reinforced

Each stage increases confidence, but no stage should pretend to solve another stage's job।


Common Mistake 1 — Bean Validation Does All Business Validation

It cannot correctly answer live questions such as Product existence or Inventory availability։


Common Mistake 2 — Database Calls Inside Custom Validators

This couples HTTP validation to persistence and creates stale check-then-act results।


Common Mistake 3 — Domain Removes Validation Because DTO Has Annotations

Internal callers could still create invalid domain state։


Common Mistake 4 — int for Required Optional-Presence Field

Missing JSON field may silently become 0

Use wrapper type when presence itself must be validated।


Common Mistake 5 — Inventing Arbitrary Maximums

Don't add maximum Product name length, order quantity, or price without requirements/design decisions।


Common Mistake 6 — Custom Validator for Duplicate Products Too Early

A simple UseCase check is clearer and works across transports։


Common Mistake 7 — Product Existence Validator on DTO

Repository-backed business checks belong in the UseCase।


Common Mistake 8 — Authorization as Validation Annotation

Roles and ownership are security/application concerns, not request-shape validation।


Common Mistake 9 — PATCH Reuses Create DTO

Create requires fields that partial update may legitimately omit।


Common Mistake 10 — Returning Raw Spring Validation Exceptions

Clients need a stable API error contract, not framework class names।


Validation Responsibility Table

RulePrimary Owner
Product create name presentRequest validation
Product create price presentRequest validation
Product request price non-negativeRequest validation
Product itself cannot hold negative priceProduct
Inventory request quantity presentRequest validation
Inventory request quantity non-negativeRequest validation
Inventory itself cannot become negativeInventory
Order request items non-emptyRequest validation + Order invariant
Order item Product ID non-blankRequest validation
Requested quantity positiveRequest validation + OrderItem invariant
Duplicate Product referencesCreateOrderUseCase + Order invariant
Product existsCreateOrderUseCase
Product activeCreateOrderUseCase using Product state
Inventory sufficientInventory coordinated by CreateOrderUseCase
Customer owns OrderUseCase / authorization
Order can be cancelledOrder
Persisted quantity integrityDatabase + domain + transaction strategy

This is the separation we want।


Request Validation Checklist

Before adding a validation rule, ask:

Can this rule be decided using only request data?

Is it part of the public request contract?

Am I accidentally querying application state?

Does the domain still need to protect this invariant?

Am I inventing a limit that requirements never defined?

Does a primitive type hide whether a field was omitted?

Is this really validation,
or is it authorization/business workflow?

Engineering Principle

The core principle:

Validate request shape at the HTTP boundary, validate live business conditions inside the UseCase, and let domain objects protect the invariants of the state they own.

Another:

Bean Validation should reject bad input early, but it should not become a hidden persistence or business-workflow layer.

And:

The closer data moves toward the domain, the stronger its guarantees should become.


Summary

In this lesson, we learned that:

  • Request validation, application validation, domain invariants, and database integrity are different concerns.
  • Jakarta Bean Validation is well suited to transport-level validation.
  • @Valid triggers validation of request DTOs in Spring MVC.
  • @NotNull, @NotBlank, @NotEmpty, @Positive, and @PositiveOrZero cover many of our current input rules.
  • Product price uses @PositiveOrZero because zero price is allowed by current requirements.
  • Product name can use @NotBlank.
  • Required numeric DTO fields often benefit from wrapper types such as Integer so missing values can be distinguished from zero.
  • SetInventoryRequest.availableQuantity should be required and non-negative.
  • CreateOrderRequest.items should be non-empty.
  • Nested CreateOrderItemRequest objects should be validated using @Valid.
  • Order quantities must be positive.
  • Bean Validation does not replace domain invariants.
  • Duplicate Product references should remain an application/domain rule rather than a premature custom Bean Validator.
  • Product existence, Product active state, and Inventory availability require live application state and belong in CreateOrderUseCase.
  • Database-backed custom validators create coupling and check-then-act problems.
  • Authorization and ownership are not Bean Validation concerns.
  • PATCH request validation differs from create-request validation because fields may be omitted.
  • Validation failures should be translated centrally into consistent 400 Bad Request responses.
  • Raw Spring validation exception names should not become public API contracts.
  • Transport validation should remain cheap, local, and side-effect free.
  • Domain objects should remain valid and testable without Spring or a Validator framework.

Next lesson:

Consistent API Error Responses

There we will design one stable error format for validation failures, missing resources, authorization failures, business conflicts, and unexpected server errors, and use centralized Spring error handling so individual Handlers do not repeat try/catch and status mapping logic.