Building REST APIs
Designing Resources and Endpoints
আপনি একটি free preview lesson দেখছেন।
আগের lesson-এ আমরা HTTP semantics দেখেছি।
এখন সেই foundation ব্যবহার করে আমাদের application-এর actual API design করতে হবে।
আমাদের main capabilities:
Product
Inventory
Order
Payment
Customer identity-ও important, কিন্তু একটি critical distinction আছে:
এই backend Customer account lifecycle own করে না।
তাই API design করার সময় আমরা blindly:
/products
/customers
/orders
/payments
এই চারটি CRUD resource বানাব না।
প্রথমে দেখব application আসলে কোন state এবং behaviour own করে।
এই lesson-এর goal:
Business capabilities থেকে clear, consistent HTTP resources এবং endpoints design করা—without exposing internal implementation or inventing a Customer subsystem.
Start From Capabilities, Not Tables
API design করার common mistake:
products table
→ /products
inventory table
→ /inventory
orders table
→ /orders
order_items table
→ /order-items
Database table থাকলেই public resource লাগবে—এমন কোনো rule নেই।
For example:
OrderItem
database-এ আলাদা rows হিসেবে persist হতে পারে।
কিন্তু current product behaviour-এ customer independently:
create OrderItem
update OrderItem
delete OrderItem
করে না।
OrderItem belongs to Order।
So:
/order-items
top-level CRUD API প্রয়োজন নেই।
Start From User and Admin Operations
Our current confirmed operations:
Customer
browse available Products
create an Order
view own Orders
view own specific Order
cancel own eligible Order
pay own eligible Order
Admin
create Product
view Product information
update Product
deactivate Product
view Inventory
set available Inventory quantity
view Orders
These operations should drive our API।
Initial Resource Map
A reasonable starting point:
/products
/inventory
/orders
Payment is primarily an operation on an existing Order in v1।
We do not currently expose Payment as a fully independent CRUD resource।
Customer is represented through authentication rather than a locally managed resource।
Customer Endpoints: What Happened to /customers?
An earlier generic milestone included:
POST /customers
GET /customers/{id}
But after requirements and architecture review, we made a stronger decision:
Customer authentication and identity lifecycle
are externally owned.
Our application does not own:
registration
password
profile lifecycle
Customer account persistence
Therefore creating:
POST /customers
would imply responsibility we explicitly do not have।
So for v1:
No Customer CRUD API.
This is not a missing endpoint।
It is a deliberate system boundary।
Authentication Supplies Customer Identity
For customer-facing operations:
HTTP Request
↓
Authentication
↓
CustomerId
↓
Handler
↓
UseCase
For example:
POST /orders
does not need:
{
"customerId": "C-123"
}
Customer identity comes from authenticated context।
This keeps ownership server-controlled।
Product Resources
Product is application-owned state।
So:
/products
is a natural resource।
We have two broad Product use cases:
customer browsing
admin management
They may share the same resource vocabulary while authorization and representation differ appropriately।
Browse Products
Customer needs to browse currently orderable Products।
A natural endpoint:
GET /products
The response should represent Products currently available for customer ordering according to our accepted definition:
Product active
AND
available Inventory > 0
This is a read operation।
So GET fits naturally।
What Does GET /products Mean?
It does not necessarily mean:
SELECT * FROM products
It means:
Return the Products exposed by the API contract.
For customer browsing, that contract may combine:
Product
+
Inventory
internally।
The API resource is not a direct database-table mirror।
Get One Product
Potentially:
GET /products/{productId}
can return one Product representation।
For customer-facing behaviour, we need to decide whether inactive/out-of-stock Products should be visible through this endpoint.
A reasonable v1 contract is:
customer-facing Product read
follows customer visibility rules
while admin access can retrieve management state through authorized admin endpoints or representation.
We should document this explicitly when implementing the API rather than letting repository behaviour decide it accidentally।
Create Product
Admin operation:
create Product
Natural endpoint:
POST /products
Request might conceptually contain:
{
"name": "Mechanical Keyboard",
"price": 100.00
}
Server determines:
Product identity
initial lifecycle state
rather than allowing arbitrary server-owned fields।
On success:
201 Created
is appropriate।
Product Creation Does Not Set Inventory Automatically Without Requirement
Important:
Creating Product does not necessarily mean:
Inventory = 100
or any invented value।
Product and Inventory are separate capabilities।
If inventory must be initialized, that behaviour should come from an explicit requirement/workflow।
Do not hide an arbitrary stock rule inside Product creation।
Update Product
Admin can update Product information।
Possible endpoint:
PATCH /products/{productId}
A partial request might later support fields such as:
{
"name": "Mechanical Keyboard Pro",
"price": 120.00
}
This maps well to:
product.rename(...);
product.changePrice(...);
inside the appropriate UseCase/domain flow।
Why PATCH Instead of Generic POST /updateProduct?
Because:
PATCH /products/{id}
communicates:
existing Product resource
partial modification
while:
POST /updateProduct
duplicates information already carried by HTTP semantics and URI।
Should Product active Be Directly Patchable?
We need to be careful।
If API accepts:
{
"active": false
}
then client appears to directly control lifecycle state।
Our domain language has an explicit operation:
product.deactivate();
The current requirement explicitly mentions:
deactivate Product
not generic arbitrary activation-state replacement।
Therefore an explicit endpoint may communicate intent better।
Product Deactivation
A reasonable API:
POST /products/{productId}/deactivate
This says:
Perform the Product deactivation business operation.
It maps naturally to:
DeactivateProductHandler
↓
DeactivateProductUseCase
↓
product.deactivate()
Why Not DELETE Product?
Because our business operation is:
deactivate
not:
physically delete
Using:
DELETE /products/{id}
may create the wrong expectation that the Product ceases to exist।
Existing Order history must remain meaningful।
So an explicit deactivation operation is clearer for v1।
What About Reactivation?
Current requirements do not explicitly include reactivation।
Therefore we should not automatically create:
POST /products/{id}/activate
just because deactivation exists।
API should reflect confirmed behaviour।
If reactivation becomes a requirement later, we can add it deliberately।
Inventory as a Resource
Inventory is application-owned state associated with Product।
Conceptually:
one Inventory state per Product
Customer does not directly modify Inventory।
Admin can:
view Inventory
set available quantity
Order workflows can internally decrease or restore quantity।
Inventory Resource Shape
A natural identifier is Product identity։
For example:
GET /inventory/{productId}
could return:
{
"productId": "P-100",
"availableQuantity": 15
}
This reflects our domain model where Inventory is naturally associated with Product।
Why Not /products/{id}/inventory?
That is also a defensible resource shape:
GET /products/{productId}/inventory
It emphasizes that Inventory belongs to Product identity।
Either structure can work।
For this course, we should choose one consistent convention rather than teaching both throughout implementation।
A clean v1 direction is:
GET /inventory/{productId}
because Inventory is a first-class capability in our application even though its identity is Product-based।
Admin Inventory Listing
Admin may need:
GET /inventory
with pagination/filtering later।
This is a collection read।
No need to expose Inventory to normal customers as a management resource।
Customer browsing receives the availability information needed through Product APIs।
Adjusting Inventory
Our accepted v1 admin behaviour:
Admin sets current available quantity to a non-negative value.
So an endpoint could be:
PUT /inventory/{productId}
with:
{
"availableQuantity": 20
}
Why can PUT fit here?
Because the operation means:
set this Inventory resource's available quantity
to the requested value
Repeating:
{
"availableQuantity": 20
}
produces the same intended final state।
This is naturally idempotent।
Why Not POST /inventory/{id}/add?
That would represent a different operation:
increase quantity by N
Repeated retries could increase Inventory repeatedly।
Our current requirement is simpler:
set available quantity
So a replacement/set-style operation is easier to reason about and safer to retry।
Inventory Update Request
Conceptually:
{
"availableQuantity": 20
}
Server/domain validates:
availableQuantity >= 0
The client does not send:
previousQuantity
as an authority unless we later design concurrency-specific semantics।
Customer Cannot Modify Inventory
Endpoints such as:
PUT /inventory/{productId}
are admin-authorized operations।
This is authorization, not a different Inventory domain model।
Security module will later enforce roles।
Orders Resource
Order is the central customer-owned resource।
Natural collection:
/orders
This supports both creation and reading depending on method।
Create Order
POST /orders
Request:
{
"items": [
{
"productId": "P-100",
"quantity": 2
},
{
"productId": "P-200",
"quantity": 1
}
]
}
Authenticated context supplies:
CustomerId
Server supplies:
current Product prices
Order total
initial UNPAID status
Order ID
On success:
201 Created
What Create Order Does Internally
The endpoint represents one business operation:
CreateOrderHandler
↓
CreateOrderUseCase
which eventually coordinates:
validate duplicate requested Products
load Products
verify active state
load Inventory
verify quantities
capture prices
decrease Inventory
construct OrderItems
construct Order
persist atomically
The API does not expose these internal implementation steps individually।
Do Not Create Endpoints for Internal Steps
Avoid APIs such as:
POST /orders/check-products
POST /orders/check-inventory
POST /orders/calculate-total
POST /orders/save
These are implementation details of one atomic operation।
The client asks:
Create this Order.
The server owns the workflow।
Order History
Authenticated customer needs own Orders।
Natural endpoint:
GET /orders
Interpretation for a customer:
Return Orders belonging to the authenticated CustomerId.
We do not need:
GET /customers/{customerId}/orders
for the normal customer flow।
Why?
Because the customer should not control whose history is requested।
Server-Controlled Ownership Scope
Bad:
GET /orders?customerId=C-999
for normal customer access।
A caller could attempt another customer's ID।
Better:
Authentication
↓
CustomerId
↓
GetOrderHistoryUseCase
The server determines the ownership filter।
Admin Order Listing
Admin may need to view all Orders।
There are multiple API design possibilities:
GET /orders
with authorization-dependent scope,
or a distinct admin-facing route/context।
We should avoid unnecessarily duplicating domain resources if authorization can clearly define access।
For now, the important rule is:
CUSTOMER
→ own Orders
ADMIN
→ broader Order access
Exact admin routing can be finalized with Security/API implementation।
Get One Order
Natural endpoint:
GET /orders/{orderId}
For a customer, the UseCase must ensure:
Order belongs to authenticated customer
Admin may have broader permission।
The Handler does not simply call:
orderRepository.findById(...)
and return whatever exists।
Ownership is part of the application contract।
Another Customer's Order
Suppose customer requests:
GET /orders/O-999
and the Order exists but belongs to somebody else।
As discussed previously, we should deliberately decide whether to return:
403
or:
404
For customer-owned resources, 404 can be useful to avoid revealing resource existence։
We can standardize this when defining the API error contract।
Order Cancellation
Cancellation is a business transition।
We do not want the client to send:
PATCH /orders/{id}
{
"status": "CANCELLED"
}
because that makes Order state appear directly mutable।
Instead, expose intent:
POST /orders/{orderId}/cancel
This maps naturally to:
CancelOrderHandler
↓
CancelOrderUseCase
↓
order.cancel()
Why POST for Cancellation?
Cancellation is a command that changes business state।
It is not a resource read।
It also involves more than changing one field:
verify ownership
validate lifecycle
restore Inventory
persist atomically
So an explicit command endpoint is clear and practical।
Is Cancellation Idempotent?
Business-state final effect might appear idempotent:
CANCELLED
stays cancelled।
But our accepted domain behaviour treats repeated cancellation as an invalid operation because:
Inventory restoration must not occur twice
Therefore we should not casually promise retry-idempotent cancellation semantics without implementing them deliberately।
The endpoint may return a business conflict if cancellation is attempted again।
Why This Matters
HTTP method choice alone does not decide retry safety।
Even:
POST /orders/{id}/cancel
can be safely implemented or poorly implemented।
Correct CancelOrderUseCase must ensure:
eligible Order transitions once
Inventory is restored once
transaction commits atomically
Paying an Order
Payment is another explicit business action।
Natural endpoint:
POST /orders/{orderId}/pay
This means:
Attempt payment for this Order.
It does not mean:
client declares Order as PAID
Payment Endpoint Flow
Conceptually:
PayOrderHandler
↓
PayOrderUseCase
↓
verify ownership
↓
verify eligible UNPAID Order
↓
PaymentService
↓
external provider
On confirmed success:
order.markPaid()
No PATCH status=PAID
Avoid:
PATCH /orders/O-1001
{
"status": "PAID"
}
This bypasses:
Payment Service
provider result
idempotency strategy
payment eligibility
and completely misrepresents the business operation।
Is Payment a Separate /payments Resource?
It could become one later if we need to expose:
Payment attempts
payment history
provider references
payment status
as independent resources।
But current scope is simpler।
Payment is currently primarily an operation attached to an Order։
So:
POST /orders/{id}/pay
is enough for the public workflow।
Do Not Invent Payment CRUD
Avoid:
POST /payments
PUT /payments/{id}
DELETE /payments/{id}
without corresponding product behaviour।
External payment integration does not automatically imply public Payment CRUD।
Current API Surface
A reasonable v1 design direction:
Products
--------
GET /products
GET /products/{productId}
POST /products
PATCH /products/{productId}
POST /products/{productId}/deactivate
Inventory
---------
GET /inventory
GET /inventory/{productId}
PUT /inventory/{productId}
Orders
------
POST /orders
GET /orders
GET /orders/{orderId}
POST /orders/{orderId}/cancel
POST /orders/{orderId}/pay
Authorization determines which caller may use each operation।
Authorization Matrix
Conceptually:
| Endpoint | CUSTOMER | ADMIN |
|---|---|---|
GET /products | Yes | Yes |
GET /products/{id} | Yes | Yes |
POST /products | No | Yes |
PATCH /products/{id} | No | Yes |
POST /products/{id}/deactivate | No | Yes |
GET /inventory | No | Yes |
GET /inventory/{productId} | No | Yes |
PUT /inventory/{productId} | No | Yes |
POST /orders | Yes | Not necessarily a normal admin operation |
GET /orders | Own orders | All/broader access |
GET /orders/{id} | Own order | Broader access |
POST /orders/{id}/cancel | Own eligible order | Depends on product requirement |
POST /orders/{id}/pay | Own eligible order | Not a normal admin operation |
We should not invent admin permissions beyond confirmed behaviour।
The important confirmed roles are:
CUSTOMER
→ create Order
→ view own Orders
ADMIN
→ manage Product
→ manage Inventory
→ view all Orders
Cancellation/payment customer behaviour is also part of our accepted scope।
Do Not Encode Roles in URIs by Default
We could create:
/admin/products
/customer/orders
But role prefixes are not always necessary।
Resources remain:
/products
/orders
and authorization can determine access।
This keeps API resource vocabulary independent from organizational role names।
When Separate Admin Routes Might Be Useful
Sometimes admin representation or workflows differ enough that:
/admin/...
becomes valuable।
But we do not need to decide that prematurely।
Start with resource semantics, then separate routes only if the contracts genuinely diverge।
OrderItem Is Nested Representation, Not Top-Level Resource
Order response may contain:
{
"id": "O-1001",
"status": "UNPAID",
"items": [
{
"productId": "P-100",
"quantity": 2,
"unitPrice": 20.00
}
],
"total": 40.00
}
This does not require:
GET /order-items/{id}
OrderItems are naturally represented inside Order।
No Endpoint to Change Order Items
Current requirements do not support:
POST /orders/{id}/items
PATCH /orders/{id}/items/{itemId}
DELETE /orders/{id}/items/{itemId}
because Order is not a Cart।
Successful Order composition is immutable in v1।
No Endpoint for Setting Order Total
Obviously avoid:
PUT /orders/{id}/total
Total is derived from OrderItems।
The server owns it।
No Endpoint for Setting Customer Ownership
Avoid:
PATCH /orders/{id}
{
"customerId": "C-200"
}
Order ownership is established at creation from authenticated identity and does not have a current reassignment workflow।
Resource Creation and IDs
For:
POST /products
and:
POST /orders
the server may generate IDs։
Exact ID strategy remains deferred।
Clients should not need to know whether we internally use:
UUID
database sequence
another identifier
The API simply returns the resource ID according to its contract।
Location Header
A successful creation response may include:
Location: /orders/O-1001
alongside:
201 Created
This can be useful because it identifies the newly created resource։
Whether we use it consistently will be decided in Handler/API implementation, but it is a natural HTTP pattern।
Request Body Should Represent Intent
Create Product:
{
"name": "Keyboard",
"price": 100.00
}
Create Order:
{
"items": [
{
"productId": "P-100",
"quantity": 2
}
]
}
Set Inventory:
{
"availableQuantity": 10
}
Notice each request contains only values the caller legitimately controls।
Avoid Generic Update DTOs
A tempting pattern:
{
"id": "...",
"type": "...",
"status": "...",
"active": "...",
"operation": "..."
}
for many endpoints।
This usually produces ambiguous contracts।
Prefer endpoint-specific request models where operations have different intent।
Endpoint-Specific DTOs
Examples:
CreateProductRequest
UpdateProductRequest
SetInventoryRequest
CreateOrderRequest
Business command endpoints such as:
cancel
pay
deactivate
may need no request body at all if all required intent is already represented by:
authenticated caller
+
resource ID
and no additional input is required।
Do Not Send Empty JSON Just Because POST Has a Body
For:
POST /orders/{id}/cancel
if no additional client input is required, we do not need:
{}
merely because it is POST।
A request body should exist when it carries meaningful input।
Payment May Need Input Later
Depending on Payment Provider contract, payment operation may eventually require:
payment method token
provider-specific payment reference
other approved payment input
We should not invent those fields now।
The endpoint shape can remain:
POST /orders/{id}/pay
while the eventual request DTO is defined from the actual provider/product requirement।
Product Browse Response
Customer-facing Product response might conceptually include:
{
"id": "P-100",
"name": "Keyboard",
"price": 100.00,
"availableQuantity": 5
}
Should availableQuantity be exposed?
Maybe।
The requirement says customers should browse useful Product information and availability।
But exact visibility of stock count is a product decision।
We should not automatically expose internal Inventory quantity if the product only needs:
in stock / out of stock
So response design must follow confirmed product needs।
Do Not Leak More Data Than Needed
This principle applies broadly।
Customer Product response does not necessarily need:
internal audit fields
database version
admin-only state
raw Inventory metadata
Order response does not need:
provider secrets
internal persistence IDs
security details
DTOs define the public contract intentionally।
Collection Endpoints Need Bounds
Both:
GET /products
and:
GET /orders
must be bounded/paginated according to our requirements।
We should not design:
GET all rows forever
even if the first development database contains ten records।
Pagination details come in a dedicated lesson।
Filtering
Potential Product/Order filtering may later use query parameters such as:
GET /products?...
GET /orders?...
But we should only add filters with real product requirements।
Do not create twenty optional query parameters upfront।
API Design and UseCases
A good endpoint should generally map to a meaningful Handler/UseCase boundary।
Examples:
POST /orders
→ CreateOrderHandler
→ CreateOrderUseCase
POST /orders/{id}/cancel
→ CancelOrderHandler
→ CancelOrderUseCase
POST /orders/{id}/pay
→ PayOrderHandler
→ PayOrderUseCase
PUT /inventory/{productId}
→ SetInventoryHandler
→ SetInventoryUseCase
Do Not Force One Handler Per HTTP Method Mechanically
We can still group closely related HTTP mappings in a Spring controller implementation if that keeps code simpler।
Our architecture terminology focuses on responsibilities:
Handler
→ transport/application entry boundary
It does not require one Java class per endpoint as a universal rule।
We will implement pragmatically while preserving:
Handler → UseCase → Repository
for meaningful workflows।
Reads May Be Simpler
For read-only queries, a UseCase may remain small:
GetOrderHistoryUseCase
→ query Repository
→ return result
That's fine।
Do not remove the application boundary just because there is little mutation logic if ownership/filtering/business query semantics still matter։
Avoid CRUD-Generated API Thinking
If we generated CRUD from tables, we might end up with:
POST /order-items
DELETE /orders
PATCH /inventory-row
POST /customers
which conflicts with actual business behaviour।
Our API instead reflects:
what customers/admins are allowed to do
That is much closer to product design।
Endpoint Naming Should Use Nouns for Resources
Prefer:
/products
/orders
/inventory
Business action suffixes are useful when the operation does not map cleanly to direct resource replacement:
/orders/{id}/cancel
/orders/{id}/pay
/products/{id}/deactivate
Avoid unnecessary verbs in standard resource operations:
/getProducts
/createProduct
/getOrder
because HTTP methods already communicate those actions।
Avoid Deep Nesting
Potentially ugly:
/customers/{customerId}/orders/{orderId}/items/{itemId}
We do not need this hierarchy।
Customer scope comes from authentication।
Order Item is not independently managed।
Simple:
/orders/{orderId}
is enough, with ownership enforced server-side।
URI Does Not Need to Encode Every Relationship
The database may contain:
Order → CustomerId
OrderItem → ProductId
but URI hierarchy does not need to reproduce all relations։
REST resources are an external API model, not a visual representation of foreign keys।
Command Endpoint Responses
For:
POST /orders/{id}/cancel
possible successful response approaches include:
200 + updated Order representation
or:
204 No Content
Both can be valid।
Returning the updated Order can be useful because client immediately sees:
status = CANCELLED
For this course, we can favor returning the resulting resource representation for important state transitions unless there is a reason not to.
Exact response contract will be handled when implementing handlers।
Payment Response
Similarly, successful:
POST /orders/{id}/pay
might return updated Order representation showing:
PAID
or a dedicated result if payment workflow requires additional public information।
Do not return raw provider response DTOs।
Provider protocol remains behind PaymentService।
External Provider Types Must Not Leak
Bad payment response:
{
"providerInternalStatus": "CAPTURED_X7",
"rawGatewayCode": "...",
"debugResponse": "..."
}
unless those are explicitly part of our API product contract।
External provider details should be mapped to application-facing concepts।
Error Semantics Are Part of Endpoint Design
For each endpoint, ask expected failures。
Example:
POST /orders
Possible business failures:
Product not found
Product inactive
duplicate Product
insufficient Inventory
invalid quantity
Example:
POST /orders/{id}/cancel
Possible:
Order not found/not accessible
Order already paid
Order already cancelled
Example:
POST /orders/{id}/pay
Possible:
Order not found/not accessible
Order already paid
Order cancelled
Payment Service failure
These must eventually map consistently to our API error model।
Do Not Design Only the Happy Path
An endpoint is not complete merely because:
POST /orders
→ 201
works।
Professional API design also defines:
what client errors mean
what business conflicts mean
what authentication failures mean
what provider/infrastructure failures mean
We will refine this throughout the module।
Endpoint Review: Products
Our current direction:
GET /products
GET /products/{productId}
POST /products
PATCH /products/{productId}
POST /products/{productId}/deactivate
Why no:
DELETE /products/{id}
Because physical deletion is not the business operation।
Why no:
POST /products/{id}/activate
Because reactivation is not yet a requirement।
Endpoint Review: Inventory
Current direction:
GET /inventory
GET /inventory/{productId}
PUT /inventory/{productId}
The update is:
set available quantity
not:
increment by arbitrary delta
for the admin API।
Order creation/cancellation modifies Inventory internally through UseCases, not through public Inventory HTTP calls।
Endpoint Review: Orders
Current direction:
POST /orders
GET /orders
GET /orders/{orderId}
POST /orders/{orderId}/cancel
POST /orders/{orderId}/pay
No:
PATCH status
edit items
set total
change owner
because those would violate our domain model।
Endpoint Review: Customers
Current direction:
No Customer CRUD endpoints.
Customer identity comes from authentication।
This is a deliberate correction from the earlier generic API milestone।
Our accepted architecture is now the source of truth।
Endpoint Review: Order Items
Current direction:
No independent OrderItem CRUD.
OrderItems are contained in Order request/response representations and managed through Order creation lifecycle।
Endpoint Review: Payments
Current direction:
POST /orders/{orderId}/pay
No general public Payment CRUD until actual payment-domain requirements justify it।
API Surface Should Stay Small
A small coherent API is better than exposing every internal capability mechanically।
Our v1 surface supports the required product behaviour without introducing:
Customer subsystem
OrderItem CRUD
Payment CRUD
Product deletion
Inventory adjustment history API
refund APIs
reservation APIs
because those do not belong to current scope।
A Useful Endpoint Design Checklist
For every proposed endpoint ask:
Which business capability owns this operation?
Is this resource actually owned by our system?
Is it a read or a mutation?
Does the HTTP method communicate that correctly?
What input does the client legitimately control?
Which values must remain server-controlled?
Does this expose a domain operation
or merely a database field mutation?
Is this resource independently managed?
Is authenticated identity being trusted
instead of request-supplied ownership?
What is the retry/idempotency behaviour?
What are the expected failures?
Does this endpoint create a feature
that is not in the requirements?
Common Mistake 1 — CRUD for Every Domain Class
OrderItem does not need public CRUD just because it exists as a domain/persistence type।
Common Mistake 2 — /customers Because Commerce Has Customers
Our backend references externally owned customer identity; it does not own Customer account lifecycle।
Common Mistake 3 — PATCH status=PAID
Payment is a workflow, not arbitrary state mutation।
Common Mistake 4 — DELETE /products
The confirmed operation is Product deactivation, not physical deletion।
Common Mistake 5 — Client-Supplied customerId
Authenticated identity controls Order ownership।
Common Mistake 6 — Client-Supplied Price or Total
Backend uses current server-side Product price and derives total।
Common Mistake 7 — Public Inventory Endpoint Used by Order Creation
Internal UseCases coordinate Inventory directly through Repository/domain logic।
We do not make our own application call its public HTTP API internally।
Common Mistake 8 — Expose Provider Payment DTOs
Third-party protocol belongs behind PaymentService।
Common Mistake 9 — Unlimited Collection Reads
Products and Orders must remain bounded/paginated।
Common Mistake 10 — Build Endpoints for Imagined Future Features
No refund, Cart editing, reservation, Product reactivation, or Customer profile API until requirements exist।
Our V1 API Direction
The current working design is:
PRODUCTS
GET /products
GET /products/{productId}
POST /products
PATCH /products/{productId}
POST /products/{productId}/deactivate
INVENTORY
GET /inventory
GET /inventory/{productId}
PUT /inventory/{productId}
ORDERS
POST /orders
GET /orders
GET /orders/{orderId}
POST /orders/{orderId}/cancel
POST /orders/{orderId}/pay
And intentionally:
NO /customers CRUD
NO /order-items CRUD
NO generic /payments CRUD
for current v1।
Engineering Principle
The core principle:
Expose the operations and resources the application actually owns—not every noun, database table, or Java class inside the implementation.
Another:
Use HTTP resources for state clients may legitimately interact with, and explicit command endpoints for meaningful business transitions such as cancellation, payment, and deactivation.
And:
Authenticated identity, authoritative prices, totals, and lifecycle transitions remain under server control.
Summary
In this lesson, we learned that:
- API resources should come from owned product capabilities, not database tables.
- Product, Inventory, and Order are meaningful API resources in the current system.
- Customer account lifecycle is externally owned, so the Order Management Backend does not expose Customer CRUD solely for ordering.
- Authenticated
CustomerIdreplaces request-controlled customer ownership. - OrderItem is an owned part of Order and does not need independent CRUD endpoints.
- Product browsing uses
GET /products. - Product creation naturally uses
POST /products. - Product partial updates can use
PATCH /products/{id}. - Product deactivation is better expressed as an explicit business operation than physical deletion.
- Inventory can be addressed through Product identity.
- Admin Inventory quantity setting can use an idempotent set-style operation such as
PUT /inventory/{productId}. - Order creation uses
POST /orders. GET /ordersfor customers should be scoped by authenticated identity rather than a client-suppliedcustomerId.- Order cancellation is an explicit lifecycle operation, not generic status mutation.
- Payment is an explicit Order operation and must not be represented as
status=PAID. - Internal Order creation steps should not become separate public endpoints.
- Order item composition, ownership, price, total, and lifecycle state remain server-controlled.
- Collection endpoints must eventually be bounded/paginated.
- Admin and customer authorization can operate over the same domain resources without automatically encoding role names in every URI.
- External payment-provider models must remain behind the
PaymentServiceboundary. - The v1 API should remain deliberately small and should not expose speculative future features.
Next lesson:
Controllers in Spring Boot
There we will map this API design into Spring Boot HTTP handlers using @RestController, request mappings, path variables, request bodies, and constructor-injected UseCases—while keeping Controllers/Handlers thin and preserving our Handler → UseCase → Repository architecture.