Building REST APIs
Request Validation
আপনি একটি 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
| Rule | Primary Owner |
|---|---|
| Product create name present | Request validation |
| Product create price present | Request validation |
| Product request price non-negative | Request validation |
| Product itself cannot hold negative price | Product |
| Inventory request quantity present | Request validation |
| Inventory request quantity non-negative | Request validation |
| Inventory itself cannot become negative | Inventory |
| Order request items non-empty | Request validation + Order invariant |
| Order item Product ID non-blank | Request validation |
| Requested quantity positive | Request validation + OrderItem invariant |
| Duplicate Product references | CreateOrderUseCase + Order invariant |
| Product exists | CreateOrderUseCase |
| Product active | CreateOrderUseCase using Product state |
| Inventory sufficient | Inventory coordinated by CreateOrderUseCase |
| Customer owns Order | UseCase / authorization |
| Order can be cancelled | Order |
| Persisted quantity integrity | Database + 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.
@Validtriggers validation of request DTOs in Spring MVC.@NotNull,@NotBlank,@NotEmpty,@Positive, and@PositiveOrZerocover many of our current input rules.- Product price uses
@PositiveOrZerobecause zero price is allowed by current requirements. - Product name can use
@NotBlank. - Required numeric DTO fields often benefit from wrapper types such as
Integerso missing values can be distinguished from zero. SetInventoryRequest.availableQuantityshould be required and non-negative.CreateOrderRequest.itemsshould be non-empty.- Nested
CreateOrderItemRequestobjects 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 Requestresponses. - 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.