Building REST APIs
HTTP Through a Backend Engineer's Eyes
So far, we have designed the internal structure of our Order Management Backend:
Handler
↓
UseCase
↓
Domain
↓
Repository
We have modeled:
Product
Inventory
Order
OrderItem
CustomerId
OrderStatus
Now we move outward.
How does an external client communicate with our application?
For our current architecture, the answer is:
HTTP
+
JSON
+
REST-style API
But understanding HTTP as a backend engineer is not about memorizing:
GET
POST
PUT
PATCH
DELETE
HTTP is part of the application's observable behaviour.
A useful mental model is:
HTTP is a behavioural contract between the client and the server.
A request communicates things such as:
Which resource am I interacting with?
What operation am I requesting?
Which values do I control?
If I repeat this request,
what effect should I expect?
How will success be represented?
How will expected failure be represented?
An API can technically work while still being confusing, unsafe, inconsistent, or difficult to integrate with.
Good backend engineering treats HTTP semantics as part of the design.
HTTP Is an Application Boundary
Our Domain does not know about HTTP.
For example:
order.cancel();
does not know whether the operation came from:
POST /api/v1/orders/{orderId}/cancel
a test, or some future internal entry point.
The HTTP boundary is responsible for:
receiving the request
understanding transport-level intent
validating transport representation
mapping external data
obtaining authenticated context
invoking the UseCase
translating result/failure
returning an HTTP response
Conceptually:
HTTP Request
↓
Handler
↓
UseCase
↓
Domain
↓
Application Result / Failure
↓
Handler
↓
HTTP Response
HTTP remains at the edge of the application.
Anatomy of an HTTP Request
Consider:
POST /api/v1/orders
Content-Type: application/json
Authorization: Bearer ...
{
"items": [
{
"productId": "59ba787c-6d83-4a21-9bae-19c3d0469bd5",
"quantity": 2
}
]
}
Several different parts are involved.
Method
POST
communicates the kind of interaction being requested.
URI
/api/v1/orders
identifies the resource or capability being addressed.
Headers
For example:
Content-Type
Authorization
Accept
carry request metadata.
Body
The JSON body carries the Order-creation input controlled by the client.
Each part has a different purpose.
HTTP Methods Communicate Intent
Common methods include:
GET
POST
PUT
PATCH
DELETE
They are not interchangeable labels.
Technically, server code could perform the same Java operation from any method.
But protocol semantics matter.
For example:
GET /api/v1/orders/{orderId}
and:
POST /api/v1/orders/{orderId}/cancel
communicate fundamentally different client intent.
The first reads state.
The second asks the system to change business state.
GET Is for Reading
A typical example is:
GET /api/v1/products
Meaning:
Give me a representation of the Products I am allowed to browse.
Likewise:
GET /api/v1/orders/{orderId}
means:
Give me this Customer-owned Order representation.
A GET request should not normally request business mutation such as:
decrease Inventory
cancel Order
create Payment
change Product price
deactivate Product
Why Mutation Through GET Is Dangerous
Imagine:
GET /api/v1/orders/{orderId}/cancel
actually cancelled the Order.
GET requests may be treated as safe reads by:
browsers
caches
proxies
monitoring systems
link preview systems
crawlers
Infrastructure may issue or repeat GET requests under the assumption that they do not request a business-state change.
Therefore:
Never hide a meaningful business mutation behind GET.
Safe HTTP Methods
A method is safe when the client is not requesting a business-state mutation.
The most important safe method for us is:
GET
Safe does not mean:
absolutely nothing changes anywhere
A GET request may still cause:
access logs
metrics
cache activity
Those are operational side effects.
But the client's requested business operation remains read-only.
Idempotency
Another crucial HTTP concept is:
If the same operation is repeated, does the intended final effect remain the same as performing it once?
That property is called:
idempotency
For example:
Set Inventory available quantity = 20
performed once:
20
and repeated several times:
20
has the same intended final state.
That operation is naturally idempotent.
By contrast:
Create another Order
performed twice may produce:
Order A
Order B
So Order creation is not naturally idempotent.
Idempotency Is About Effect
Idempotency does not require the exact same response every time.
Suppose an idempotent resource-removal operation succeeds once.
A repeated request could potentially return a different response because the resource is already gone.
The important property is:
after one successful application
→ target state reached
after repeated applications
→ target state remains reached
Think about the resulting system effect, not byte-for-byte response equality.
Why Idempotency Matters in Distributed Systems
Networks fail ambiguously.
Imagine:
Client
↓
POST /api/v1/orders
↓
Server creates and commits Order
↓
Response connection breaks
The client sees:
timeout
What does that prove?
Only:
The client did not receive a successful response.
Possible realities include:
request never reached server
request reached server but failed
Order was successfully committed
but the response was lost
If the client blindly retries:
POST /api/v1/orders
the system may create a second Order.
This is why retry behaviour and idempotency must be designed deliberately.
POST
POST commonly represents:
creating a new subordinate resource
triggering a command-like operation
performing an operation that is
not naturally idempotent
Our canonical Order creation endpoint is:
POST /api/v1/orders
The client expresses:
Create an Order containing
these requested Products and quantities.
The server creates a new:
Order
with server-owned identity, ownership, price, time, and lifecycle state.
POST Is Not "Anything That Changes Data"
A weak API might contain:
POST /getProducts
POST /updateProduct
POST /deleteProduct
POST /cancelOrder
Everything becomes POST because implementation is easy.
But then much of HTTP's semantic value is lost.
A client can no longer infer much from:
method + URI
Our API should remain resource-oriented and operation-aware.
PUT
PUT generally represents setting or replacing the state of a resource at a known URI.
The useful property for us is:
repeating the same replacement
should result in the same intended state
Our Inventory API is a good example.
Canonical endpoint:
PUT /api/v1/inventory/{productId}
with a body conceptually like:
{
"availableQuantity": 20
}
means:
Set the current available quantity for this Product to exactly 20.
Calling it once or repeatedly produces the same intended final quantity:
20
This aligns naturally with our:
SetInventoryUseCase
semantics.
PUT Is Not "Increment"
Suppose instead the request meant:
{
"increaseBy": 20
}
Then repeated requests would produce:
10 → 30 → 50
That would not be idempotent.
The method name alone does not create idempotency.
The implementation and business operation must honour the contract.
PATCH
PATCH represents partial modification of an existing resource.
Our canonical Product update endpoint is:
PATCH /api/v1/products/{productId}
The request may contain:
{
"priceCents": 8990
}
Meaning:
Change this Product's current price to 8990 cents while leaving omitted fields unchanged.
Our established Product PATCH semantics are:
name omitted/null
→ unchanged
priceCents omitted/null
→ unchanged
{}
→ accepted no-op
This is a partial update rather than full replacement.
PATCH Is Not Automatically Idempotent
Our Product update:
{
"priceCents": 8990
}
means:
set priceCents = 8990
Applying it repeatedly results in:
8990
So that particular operation is idempotent in effect.
But imagine:
{
"increasePriceCentsBy": 1000
}
Then repeated execution would produce:
8990 → 9990 → 10990
That operation would not be idempotent.
Again:
The business meaning determines the effect.
The HTTP method communicates expectations but cannot fix incorrect semantics automatically.
DELETE
DELETE usually communicates:
Remove this resource.
But our Product Domain does not support physical Product deletion.
Our business operation is:
deactivate Product
not:
delete Product
Therefore blindly exposing:
DELETE /api/v1/products/{productId}
could communicate stronger semantics than the business actually provides.
Our canonical API uses:
POST /api/v1/products/{productId}/deactivate
This makes the business operation explicit.
HTTP Method Does Not Replace Domain Meaning
An API could theoretically represent Product deactivation as:
PATCH /api/v1/products/{productId}
with:
{
"active": false
}
But our Product lifecycle deliberately does not expose arbitrary active-state assignment.
There is no v1 reactivation operation.
So an explicit:
POST /api/v1/products/{productId}/deactivate
better matches:
product.deactivate();
The HTTP contract should preserve server-controlled Domain meaning.
Resource-Oriented Thinking
Our primary API resources are:
products
inventory
orders
Examples:
GET /api/v1/products
GET /api/v1/products/{productId}
POST /api/v1/orders
GET /api/v1/orders
GET /api/v1/orders/{orderId}
These are preferable to RPC-like paths such as:
/getAllProducts
/createNewOrder
/findOrderById
because the HTTP method already carries part of the operation semantics.
URI Identifies the Resource, Not Our Java Architecture
Good:
/api/v1/orders
/api/v1/products
Bad:
/orderHandler/create
/productRepository/findAll
/createOrderUseCase
External clients should not know whether our implementation contains:
Handler
UseCase
Repository
JPA
PostgreSQL
Those are internal implementation details.
Consistent Resource Naming
Our API consistently uses plural resource names where appropriate:
/products
/orders
Inventory is modeled as its own resource collection:
/inventory
Consistency makes the API easier to predict.
The goal is not to endlessly debate singular vs plural naming.
The goal is a coherent contract.
Path Parameters
For a specific Order:
GET /api/v1/orders/{orderId}
The path identifies:
which Order
At the HTTP boundary, the value is a string.
For example:
d2e7b44e-463f-4bbc-b16f-b1c80bc80442
The Handler parses it into:
UUID
↓
OrderId
The Domain receives a typed identity rather than HTTP path text.
Query Parameters
Query parameters commonly adjust collection reads.
For example:
GET /api/v1/products?page=0&size=20
Our canonical pagination semantics are:
page
→ zero-based
→ default 0
size
→ default 20
→ minimum 1
→ maximum 100
Query parameters are useful for:
pagination
filtering
sorting
where those features exist.
They should not secretly trigger state-changing business commands.
Never Hide Mutation in a Query Parameter
Avoid:
GET /api/v1/orders/{orderId}?cancel=true
The request appears to be a read while a query parameter secretly changes business state.
Our cancellation operation is explicit:
POST /api/v1/orders/{orderId}/cancel
Headers
HTTP headers carry metadata about the request or response.
Relevant examples include:
Content-Type
Authorization
Accept
request/correlation identifiers
For JSON requests:
Content-Type: application/json
communicates the representation format.
Our v1 API primarily uses:
application/json
We do not need a complicated content-negotiation model.
Authorization Header
Authentication credentials commonly arrive through:
Authorization: Bearer ...
The Handler and Domain should not repeatedly parse this header.
The intended flow is:
HTTP Authorization Header
↓
Spring Security
↓
Authenticated identity
↓
CustomerId / authorities
↓
Handler / UseCase
The application receives meaningful identity.
It does not receive raw authentication protocol details unless the security boundary specifically needs them.
HTTP Status Codes Are Part of the Contract
An HTTP response includes more than JSON.
For example:
HTTP/1.1 200 OK
or:
HTTP/1.1 404 Not Found
The status communicates broad outcome semantics before the client interprets the response body.
Using status codes consistently makes integrations easier.
200 OK
200 OK is appropriate when an operation succeeds and returns a representation or result.
For example:
GET /api/v1/orders/{orderId}
may return:
200 OK
with the Order representation.
Likewise our established Product update/deactivation APIs may return 200 with the current Product representation when useful.
201 Created
When a new resource is successfully created:
POST /api/v1/orders
a natural success response is:
201 Created
The server may return the created Order representation.
This communicates more accurately than returning generic 200 for every success.
204 No Content
204 No Content means:
operation succeeded
+
response intentionally has no body
It is useful for APIs whose successful mutation returns nothing.
But:
Never return
204and also attempt to send a JSON response body.
If the updated representation is useful, use an appropriate body-bearing success response instead.
400 Bad Request
Our canonical API uses 400 for invalid request input.
Examples:
malformed UUID
missing required field
invalid request structure
quantity <= 0
negative priceCents
Our canonical validation Problem Details uses:
VALIDATION_ERROR
with field-level errors where relevant.
Not every business rejection is a 400.
401 Unauthorized
Despite the HTTP name, 401 generally means:
valid authentication
was not established
Examples:
missing credential
invalid token
expired/unacceptable token
Our API maps this to:
AUTHENTICATION_REQUIRED
The operation does not proceed as an authenticated Customer/Admin workflow.
403 Forbidden
403 means the caller may be authenticated but lacks authority for the requested operation.
Example:
CUSTOMER
attempts an Admin-only
Product-management operation
This maps naturally to:
ACCESS_DENIED
Authentication and authorization are different concepts.
401 vs 403
A useful distinction is:
401
→ Who are you?
cannot be established acceptably
403
→ We know who you are,
but you cannot perform this operation
Do not treat them as interchangeable.
404 Not Found
For a missing resource:
GET /api/v1/orders/{orderId}
may return:
404 Not Found
with:
ORDER_NOT_FOUND
Our Customer-facing Order API makes an additional deliberate security decision.
If:
Order does not exist
or:
Order exists
but belongs to another Customer
the external result is the same:
404
ORDER_NOT_FOUND
This avoids leaking whether another Customer's protected resource exists.
Resource Ownership and 404
Customer-facing Order reads use authenticated ownership scope.
For example:
GET /api/v1/orders/{orderId}
means:
Give me this Order if it is one of my Orders.
It does not mean:
Tell me whether any Customer has an Order with this ID.
This is why 404 is the correct canonical result for both missing and non-owned Orders in our API.
409 Conflict
A request may be structurally valid but incompatible with current business state.
Examples:
cancel a PAID Order
pay a CANCELLED Order
insufficient Inventory
These are not malformed JSON.
They are business-state conflicts.
Our canonical mappings include:
PRODUCT_NOT_ORDERABLE
→ 409
INSUFFICIENT_INVENTORY
→ 409
ORDER_NOT_CANCELLABLE
→ 409
ORDER_NOT_PAYABLE
→ 409
Why We Are Not Using Every Possible 4xx Code
Some APIs use:
422 Unprocessable Content
for certain semantic-validation failures.
Our goal is not to maximize status-code variety.
Our goals are:
clarity
consistency
predictability
We already have a canonical mapping model for this course.
Handlers should follow it rather than choosing statuses ad hoc.
5xx Means Server-Side Failure
Broadly:
5xx
means the server failed to fulfill an otherwise valid operation because of an internal problem.
Examples:
unexpected bug
database unavailable
broken persistence integrity
unhandled infrastructure failure
An expected Domain rejection does not become 500 simply because Java represented it with an exception.
Exception Does Not Mean 500
For example:
OrderNotCancellable
is an expected business rejection.
The Domain expresses the business failure.
The API boundary translates it into:
409
ORDER_NOT_CANCELLABLE
Unexpected failures map to:
500
INTERNAL_ERROR
This separation keeps HTTP concerns outside the Domain.
Structured Error Responses
We do not want random responses such as:
"bad request"
from one endpoint and:
"something went wrong"
from another.
Our canonical API uses an RFC 7807-style Problem Details contract.
For example:
{
"type": "https://api.tarkev.com/problems/validation-error",
"title": "Validation failed",
"status": 400,
"detail": "One or more fields are invalid.",
"instance": "/api/v1/products",
"code": "VALIDATION_ERROR",
"errors": [
{
"field": "name",
"code": "REQUIRED",
"message": "Name is required."
}
]
}
The exact mapping implementation comes later in this module.
The important point is:
HTTP status
+
stable application error code
+
structured response
work together.
Never Return Internal Exceptions to Clients
Bad:
{
"error": "org.hibernate.exception.ConstraintViolationException..."
}
Clients should not receive:
SQL
stack traces
internal Java class names
database implementation details
secret configuration
API errors should communicate safe, actionable client-facing meaning.
Never Return 200 OK for Every Failure
Weak:
200 OK
with:
{
"success": false,
"error": "Order not found"
}
Now the client must ignore normal HTTP outcome semantics and inspect a custom field for every operation.
Better:
404
+
ORDER_NOT_FOUND
+
Problem Details body
Use both protocol semantics and structured application error information.
Status Code Alone Is Not Enough
A 400 only tells the client broadly that the request is invalid.
For validation, the client may also need:
error code
human-readable detail
specific field
field-level code
That is why our Problem Details representation includes:
code
errors[]
where appropriate.
HTTP Contract Is Not the Domain Model
Our Domain uses:
OrderStatus.UNPAID
The API may expose:
{
"status": "UNPAID"
}
But that representation belongs to the DTO/API contract.
The Domain does not need Jackson annotations or HTTP concerns simply to make this JSON work.
Server Authority Must Be Preserved
Consider Create Order.
The client controls:
ProductId
quantity
The server controls:
CustomerId
OrderId
OrderStatus
unitPriceCents
createdAt
totalCents
That distinction must be visible in the HTTP contract.
Bad request design:
{
"customerId": "another-customer",
"status": "PAID",
"totalCents": 100,
"items": [
{
"productId": "...",
"quantity": 2,
"unitPriceCents": 1
}
]
}
The client must not control those values.
Clients Express Intent
A better API lets clients say:
Create this Order.
Cancel my Order.
Pay my Order.
The server decides:
Who owns it?
What does it cost?
Is the transition legal?
Can Inventory be consumed?
Has Payment succeeded?
What state should be persisted?
This aligns HTTP with our Domain architecture.
Explicit Business Commands
Some operations map naturally to ordinary resource CRUD-like behaviour.
For example:
create Product
read Product
update Product
Other operations are meaningful state transitions:
deactivate Product
cancel Order
pay Order
Trying to expose every operation as generic field mutation can make the API less accurate.
Order Cancellation
Our canonical cancellation endpoint is:
POST /api/v1/orders/{orderId}/cancel
This means:
Attempt the business operation of cancelling this Customer-owned Order.
Compare with:
PATCH /api/v1/orders/{orderId}
and:
{
"status": "CANCELLED"
}
The second representation suggests the client can directly assign Order status.
But in our Domain:
order.cancel();
contains business rules.
So the explicit command endpoint better preserves that meaning.
Payment
Our canonical Payment command is:
POST /api/v1/orders/{orderId}/pay
This means:
Attempt to pay this Order.
It does not mean:
set Order status = PAID
The client cannot simply request:
{
"status": "PAID"
}
because Payment requires:
authenticated ownership
Order eligibility
PaymentService
external provider result
confirmed successful outcome
current Order-state recheck
Only then may application code call:
order.markPaid();
Command vs State Assignment
This distinction is important.
Set status to PAID
is raw state assignment.
Pay this Order
is a business command.
Likewise:
Set status to CANCELLED
is weaker than:
Cancel this Order
HTTP should communicate business intent rather than exposing arbitrary Domain mutation.
Repeating POST /orders
Without a deliberate idempotency mechanism:
POST /api/v1/orders
sent twice may create:
two separate Orders
even if the request bodies are identical.
The API must not pretend otherwise.
If retry-safe Order creation becomes a requirement, we would introduce an explicit idempotency contract.
We do not invent one silently.
Payment Is More Sensitive
Repeated:
POST /api/v1/orders/{orderId}/pay
can have financial consequences.
Our Domain already prevents:
PAID → PAID
through:
order.markPaid();
But that does not guarantee the Payment provider was not called twice.
External Payment idempotency requires dedicated application/provider design.
HTTP method choice alone does not solve it.
Idempotency Is a System Property
Saying:
PUT is idempotent
does not make implementation magically correct.
If the Handler increments something on every identical PUT, the endpoint violates the expected semantics.
Likewise:
POST is normally non-idempotent
does not prevent an application from deliberately implementing idempotency using an idempotency key.
Protocol semantics and implementation must agree.
HTTP Timeout Does Not Mean the Operation Failed
This is one of the most important distributed-system ideas to understand.
Suppose:
Client
↓
POST /api/v1/orders
↓
timeout
The client knows:
I did not receive the success response.
It does not know:
The Order was definitely not created.
The server may already have committed successfully.
This is why retries must be designed based on operation semantics, not assumptions.
Error Response vs Lost Response
Case A:
server returns 400
client receives 400
The result is known.
Case B:
server commits Order
response is lost in network
client sees timeout
The business operation may already have succeeded.
These cases are fundamentally different.
HTTP and Caching
GET responses may participate in caching depending on headers, proxies, or infrastructure.
We do not need to implement caching in this module.
But this reinforces why GET should accurately represent read semantics.
Protocol infrastructure uses the method as part of its behaviour.
Avoid GET Request Bodies
Even if a particular stack technically supports a body on GET, do not build ordinary resource-query APIs around it.
Use:
path parameters
query parameters
for normal GET input.
Prefer conventional HTTP patterns unless a strong requirement says otherwise.
HTTP Is Stateless at the Interaction Level
A REST-style request should contain or reference enough information for the server to perform the operation without depending on hidden conversational state inside one application process.
For example:
Request 1
→ create Order
Request 2
→ pay Order
The second request identifies:
OrderId
authenticated caller
through explicit application/request state.
It must not depend on:
the same JVM remembering
an Order object from Request 1
Durable state belongs in PostgreSQL.
Authentication context comes with the request.
Never Store Per-Customer Request State in Singleton Beans
Bad:
@Component
public class OrderHandler {
private Order currentOrder;
}
Spring application components are normally shared across many requests.
One Customer's business state must not be stored in shared mutable Bean fields.
Use:
method-local values
Domain objects
persistent database state
instead.
HTTP Request Boundary Is Not the Database Transaction Boundary
An HTTP request and a database transaction are related but different concepts.
For example:
POST /api/v1/orders
↓
CreateOrderHandler
↓
CreateOrderUseCase
may execute the local Order creation workflow transactionally.
But HTTP itself does not provide atomicity.
Transaction boundaries belong to the application workflow.
Observable Contract
Internally, one request may involve:
multiple classes
several Repository operations
a transaction
Domain construction
The client should not need to know those details.
The client observes:
method
URI
headers
request body
status code
response body
That is the public API contract.
Internal Refactoring Should Not Rewrite the Public Contract
Suppose we later optimize:
Repository implementation
query strategy
Domain persistence mapping
If Product behaviour has not changed, the client should not need an unnecessary API migration.
Keeping transport and implementation separate gives us this flexibility.
API Design Is Product Design
Consider cancellation.
We must decide:
Which Orders can be cancelled?
How does a client request cancellation?
What means success?
What happens if Order is already PAID?
What happens if Order belongs to another Customer?
Those are not merely Spring Controller details.
They are product behaviour exposed through an API.
Do Not Design Endpoints From Repository Methods
Bad reasoning:
OrderRepository.save()
→ POST /saveOrder
or:
ProductRepository.findAll()
→ GET /findAllProducts
Persistence implementation should not define the public contract.
Start from:
resources
business capabilities
use cases
Do Not Design Endpoints Around Java Class Names
Avoid:
/createOrderUseCase
/orderHandler
/productEntity
Public vocabulary should remain:
/orders
/products
/inventory
Internal architecture is not part of the client-facing API.
Predictability Matters
If:
GET /api/v1/products/{productId}
retrieves one Product, then:
GET /api/v1/orders/{orderId}
should follow a similar resource pattern where appropriate.
Consistency reduces documentation burden because clients can predict common behaviour.
Consistency Beats Cleverness
Avoid an API where:
Order cancellation
→ /orders/{id}/cancel
Product deactivation
→ /products?action=disable
Inventory update
→ /updateInventory
without a deliberate reason.
Use a coherent vocabulary across the API.
Our canonical surface does that.
HTTP Does Not Replace Business Documentation
Even a conventional endpoint:
POST /api/v1/orders
does not tell a client that:
duplicate ProductIds are rejected
Products must exist
Products must be active
server-side current price is authoritative
Inventory is consumed at Order creation
all Items succeed atomically
new Order status is UNPAID
These rules still need to be documented through:
OpenAPI
API documentation
product requirements
HTTP semantics provide structure.
They do not describe the entire business contract.
Create Order Contract
Conceptually:
POST /api/v1/orders
Request:
{
"items": [
{
"productId": "59ba787c-6d83-4a21-9bae-19c3d0469bd5",
"quantity": 2
}
]
}
The server determines:
CustomerId
→ authenticated caller
OrderId
→ generated UUID
unitPriceCents
→ current authoritative Product price
status
→ UNPAID
createdAt
→ application Clock
totalCents
→ derived from OrderItems
Successful creation:
201 Created
This is far more meaningful than:
POST /createOrder
returning 200 for every outcome.
Browse Products Contract
Conceptually:
GET /api/v1/products
This is a safe read.
The Customer-visible result follows the business rule:
Product active = true
AND
Inventory availableQuantity > 0
The client does not need to know whether the backend used:
join
projection
multiple internal objects
The API returns the representation produced by the business query.
Own Order History
Canonical endpoint:
GET /api/v1/orders
For an authenticated Customer, this means:
Return my Order history.
Ownership comes from authentication.
We do not need:
GET /api/v1/customers/{customerId}/orders
for the Customer-facing API because the caller should not control arbitrary Customer ownership scope.
Customer and Admin Access Can Share Domain Resources
An Admin may later be allowed to view a broader Order set than a Customer.
That does not require two different Order Domain models.
Security and application queries determine the permitted scope.
The underlying Order remains the same business concept.
Domain Error → HTTP Mapping
Our current canonical mapping includes:
invalid request
→ 400
authentication missing/invalid
→ 401
authenticated caller lacks authority
→ 403
Product not found
→ 404
Order missing or not owned
→ 404
Product not orderable
→ 409
insufficient Inventory
→ 409
Order not cancellable
→ 409
Order not payable
→ 409
unexpected server failure
→ 500
Handlers should not invent different semantics independently.
HTTP Semantics Should Match UseCase Semantics
Our internal operation:
CreateOrderUseCase
maps naturally to:
POST /api/v1/orders
Internal:
GetOrderHistoryUseCase
maps naturally to:
GET /api/v1/orders
Internal:
CancelOrderUseCase
maps naturally to:
POST /api/v1/orders/{orderId}/cancel
Internal:
PayOrderUseCase
maps naturally to:
POST /api/v1/orders/{orderId}/pay
This alignment makes the system easier to understand from HTTP all the way down to the Domain.
A Useful Endpoint Design Sequence
When designing an API endpoint, do not begin with:
Which Spring annotation should I use?
Instead ask:
1. What business operation or resource is involved?
2. Is it a read or a mutation?
3. What does the client control?
4. What remains server-controlled?
5. Is the operation naturally idempotent?
6. What URI best represents the resource or operation?
7. What successful outcome should the client observe?
8. What expected failures exist?
9. Which HTTP semantics represent those outcomes?
Only then should we implement the Handler.
Spring Annotation Comes Last
After the contract is understood, we choose implementation syntax such as:
@GetMapping(...)
or:
@PostMapping(...)
The annotation does not design the API.
It implements the API design.
Common Mistake — Every Endpoint Uses POST
If every endpoint is POST, we throw away useful protocol semantics.
Common Mistake — Mutation Through GET
Never use safe-read semantics for Order cancellation, Payment, Product changes, or Inventory mutation.
Common Mistake — Client Controls Server-Owned State
The client must not control:
Order status
Order total
Customer ownership
purchase-time price
createdAt
OrderId
for Order creation.
Common Mistake — Every Failure Returns 500
Expected business rejection is not an internal server crash.
Map expected failures deliberately.
Common Mistake — Every Success Returns 200
Creation, successful reads, and no-content operations have different semantics.
Use HTTP deliberately.
Common Mistake — Endpoint Names Mirror Java Methods
Avoid:
/getAllProducts
/createOrderUseCase
/setOrderStatus
Use resource and business language.
Common Mistake — DELETE for Deactivation Without Thinking
Product deactivation is not physical deletion.
Our API reflects the actual Product operation.
Common Mistake — Assuming POST Retry Is Safe
A repeated Order-creation POST can create duplicate Orders unless an explicit idempotency mechanism exists.
Common Mistake — Assuming Domain Validation Solves Request Retry
Domain invariants can protect one Order's validity.
They do not automatically prevent:
duplicate Order creation
double Payment-provider execution
Retry safety is a broader system concern.
Common Mistake — Designing APIs From Database Tables
Public API behaviour begins from:
business resources
use cases
client intent
not from automatically exposing CRUD for every table.
HTTP Review Checklist
Before approving an endpoint, ask:
Does the method match the operation semantics?
Is GET free from requested business mutation?
Is the URI resource-oriented and consistent?
Does the client control only legitimate input?
Are server-owned values generated/derived server-side?
Is retry behaviour understood?
Is the operation naturally idempotent?
Does implementation actually preserve that property?
Does success use an appropriate status?
Are expected failures distinguishable?
Could the response leak another Customer's
resource existence?
Do HTTP concerns stay at the Handler boundary?
How HTTP Fits Our Architecture
The complete direction is:
HTTP Method + URI + Headers + JSON
↓
Handler
↓
UseCase
↓
Domain
↓
Repository / External Service
Then:
Application Result / Failure
↓
Handler / Error Mapping
↓
HTTP Status + JSON
HTTP is our external contract.
It does not replace the application or Domain architecture.
Our API Design Principles
For this backend, we will follow these principles:
Use resource-oriented URIs.
Use HTTP methods according
to their intended semantics.
Never request business mutation through GET.
Keep authenticated ownership server-controlled.
Keep authoritative prices server-controlled.
Keep totals server-controlled.
Keep lifecycle state transitions server-controlled.
Use explicit business commands where
generic field mutation would weaken meaning.
Use meaningful success status codes.
Use consistent HTTP error mappings.
Keep request/response DTOs separate
from Domain behaviour.
Treat retry and idempotency
as real distributed-system concerns.
Prefer conventional predictable APIs
over clever endpoint designs.
These principles guide the rest of Module 5.
Engineering Principle
The core principle is:
HTTP is not merely a transport pipe; its methods, resource semantics, status codes, and retry behaviour are part of the application's public behavioural contract.
Another:
Clients express intent through the API while the backend remains authoritative over business identity, pricing, totals, ownership, and valid state transitions.
And:
Design the operation first, choose the HTTP representation second, and write the Spring annotation last.
Summary
In this lesson, we established that:
- HTTP is an external application contract, not merely a set of Spring annotations.
- Requests use methods, URIs, headers, and optional bodies to communicate intent.
- The Handler owns the HTTP/application boundary.
- Domain objects remain transport-independent.
- GET represents safe read behaviour and must not request business mutation.
- Safe methods and idempotent operations are different concepts.
- Idempotency describes the intended final state/effect of repeated execution.
- Idempotency does not require byte-for-byte identical responses.
- Network timeouts may leave operation outcome ambiguous.
POST /api/v1/ordersis appropriate for creating a new Order.- Repeated Order-creation POSTs may create multiple Orders without an explicit idempotency contract.
PUT /api/v1/inventory/{productId}fits our exact-quantity replacement semantics and is naturally idempotent.PATCH /api/v1/products/{productId}represents our partial Product update contract.- PATCH idempotency depends on the actual patch semantics.
- Product deactivation should not be represented as physical deletion.
POST /api/v1/products/{productId}/deactivatematches our explicit Domain operation.- Resource-oriented URIs are preferable to Java/RPC-style endpoint names.
- UUIDs are strings at the HTTP boundary and are parsed into typed Domain IDs.
- Query parameters are suitable for pagination/filtering/sorting, not hidden mutation.
- Authorization metadata is handled by HTTP/security infrastructure and becomes application-friendly identity.
200,201, and204describe different kinds of success.POST /api/v1/ordersreturns201 Createdon successful creation.400represents malformed or invalid input according to our API contract.401represents authentication not being established.403represents an authenticated caller lacking permission for an operation.404represents missing Products/Orders where appropriate.- Missing and non-owned Customer Orders both produce
404 / ORDER_NOT_FOUND. 409represents our defined business-state conflicts such as insufficient Inventory and invalid Order transitions.- Expected business failures do not become
500merely because they are exceptions internally. - Unexpected server/infrastructure failures map to
500 / INTERNAL_ERROR. - API errors use structured Problem Details rather than arbitrary strings or internal exception text.
- The Problem Details base remains
https://api.tarkev.com/problems/.... - HTTP response status and structured application error code work together.
- Request/response DTOs remain separate from Domain objects.
- Clients cannot control Customer ownership, Order status, authoritative price, purchase-time price, total, ID, or creation time.
- Cancellation is a business command rather than generic Order-status assignment.
- Payment is a business command rather than allowing the client to set
PAID. POST /api/v1/orders/{orderId}/cancelmatchesCancelOrderUseCase.POST /api/v1/orders/{orderId}/paymatchesPayOrderUseCase.- Order lifecycle idempotency and external Payment idempotency are different concerns.
- HTTP method conventions do not automatically make an implementation safe or idempotent.
- HTTP requests do not rely on hidden per-customer state inside singleton Spring Beans.
- The HTTP request boundary and database transaction boundary are distinct.
- A client timeout does not prove the server-side operation failed.
- The public API should remain stable across internal refactoring when product behaviour does not change.
- API design should begin from business resources and UseCases, not Repository methods or Java class names.
- Conventional and consistent APIs are easier to integrate with than clever but inconsistent designs.
- HTTP semantics structure the contract but do not replace documentation of business rules.
- Spring annotations are implementation details chosen after the HTTP contract has been designed.
Next lesson:
Designing Resources and Endpoints
There, we will design the concrete API surface for Products, Inventory, and Orders, including our already established Product-management endpoints, exact Inventory replacement, Customer-owned Order history/detail, Order creation, explicit cancellation/payment commands, and the decision to expose no local Customer resource solely for ordering.
