Resource vs Service API Design - Complete Interview and Production Guide
Learn resource-oriented and service-oriented API design, REST resource modeling, URI design, action endpoints, Spring Boot implementation, production scenarios, common mistakes, best practices, and 15 interview questions with answers.
Introduction
One of the most important API design decisions is determining whether an endpoint should represent:
- A resource
- A business service
- A command
- A state transition
- A long-running process
Resource-oriented APIs expose business concepts as nouns.
Service-oriented APIs expose operations or capabilities as commands.
Resource-oriented example:
POST /api/v1/payments
Service-oriented example:
POST /api/v1/processPayment
Both endpoints can technically process a payment.
However, they communicate different architectural styles.
A well-designed REST API usually prefers resources and HTTP semantics. Real enterprise systems, however, also contain complex business operations that may not fit simple CRUD operations.
The goal is not to eliminate every verb.
The goal is to choose the clearest model for the business capability.
What Is a Resource?
A resource is a business entity, concept, collection, event, or process that can be uniquely identified through a URI.
Examples:
- Customer
- Account
- Order
- Payment
- Claim
- Policy
- Employee
- Product
- Invoice
- Transfer
- Report
- Cancellation request
Example:
GET /api/v1/customers/101
Response:
{
"id": 101,
"name": "Venu",
"email": "[email protected]",
"status": "ACTIVE"
}
Here:
/customers/101
identifies customer resource 101.
Characteristics of a Resource
A resource usually has:
- An identity
- A URI
- State
- A representation
- A lifecycle
- Relationships with other resources
- Operations performed through HTTP methods
Example order resource:
{
"orderId": "ORD-1001",
"customerId": "CUS-101",
"amount": 250.00,
"currency": "USD",
"status": "CREATED"
}
This order has:
- Identity:
ORD-1001 - State:
CREATED - Representation: JSON
- Lifecycle: Created → Paid → Shipped → Delivered
What Is a Service?
A service represents a business operation, function, or capability.
Examples:
- Calculate premium
- Validate an address
- Reset a password
- Generate a report
- Approve a claim
- Close an account
- Transfer funds
- Capture a payment
- Recalculate a balance
Example:
POST /api/v1/calculatePremium
Request:
{
"customerAge": 35,
"coverageAmount": 500000,
"policyType": "TERM"
}
Response:
{
"monthlyPremium": 82.50,
"currency": "USD"
}
The endpoint is centered around executing an operation rather than retrieving or managing a persistent resource.
Resource-Oriented vs Service-Oriented API
| Area | Resource-Oriented API | Service-Oriented API |
|---|---|---|
| Primary focus | Business entity or state | Business operation |
| URI style | Usually nouns | Often verbs |
| Example | /orders/101 |
/cancelOrder |
| HTTP method | Communicates operation | URI often communicates operation |
| State | Central to design | May be secondary |
| CRUD support | Natural | Often custom |
| Caching | Easier for retrieval | Less common |
| Idempotency | Often clearer | Must be explicitly designed |
| REST alignment | Strong | May resemble RPC |
| Best use | Resources and state transitions | Commands, calculations, workflows |
Resource-Oriented API Design
Resource-oriented design models the business domain using nouns.
Examples:
/customers
/accounts
/orders
/payments
/claims
/policies
HTTP methods communicate the intended operation.
| Requirement | Method | Endpoint |
|---|---|---|
| Get all customers | GET | /customers |
| Get one customer | GET | /customers/{id} |
| Create customer | POST | /customers |
| Replace customer | PUT | /customers/{id} |
| Partially update customer | PATCH | /customers/{id} |
| Delete customer | DELETE | /customers/{id} |
Service-Oriented API Design
Service-oriented APIs expose operations directly.
Examples:
POST /api/v1/createCustomer
POST /api/v1/findCustomer
POST /api/v1/updateCustomer
POST /api/v1/deleteCustomer
This style may resemble Remote Procedure Call.
The consumer tells the server exactly which function to execute.
Resource-Oriented Alternative
Instead of:
POST /api/v1/createCustomer
use:
POST /api/v1/customers
Instead of:
POST /api/v1/getCustomer
use:
GET /api/v1/customers/101
Instead of:
POST /api/v1/updateCustomer
use:
PUT /api/v1/customers/101
or:
PATCH /api/v1/customers/101
Instead of:
POST /api/v1/deleteCustomer
use:
DELETE /api/v1/customers/101
Why REST Prefers Resources
REST uses a uniform interface.
The URI identifies the target.
URI
Identifies what resource is involved
The HTTP method identifies the operation.
HTTP Method
Describes what should happen
Example:
GET /api/v1/orders/ORD-1001
Meaning:
GET
Retrieve
/orders/ORD-1001
Order ORD-1001
This makes APIs predictable and consistent.
Uniform Interface
REST uses standard HTTP methods across different resources.
GET
Retrieve a representation
POST
Create or submit processing
PUT
Replace a resource
PATCH
Partially modify a resource
DELETE
Remove a resource
Consumers do not need to learn a custom operation name for every endpoint.
Resource Naming Rules
Use nouns for resource names.
Recommended:
/customers
/orders
/payments
/products
/claims
Avoid verbs for standard resource operations.
Avoid:
/getCustomers
/createOrder
/deletePayment
/updateProduct
The HTTP method already communicates the action.
Plural Resource Names
Plural resource names are commonly used for collections.
Recommended:
/customers
Specific customer:
/customers/101
This creates a natural relationship:
Customer Collection
↓
Individual Customer
Avoid mixing singular and plural naming.
Inconsistent:
/customer/101
/orders/1001
/product/501
Consistent:
/customers/101
/orders/1001
/products/501
Collection Resource
A collection represents multiple resources.
Example:
GET /api/v1/customers
Response:
{
"items": [
{
"id": 101,
"name": "Venu"
},
{
"id": 102,
"name": "John"
}
],
"page": 0,
"size": 20,
"totalElements": 2
}
Create a new resource under the collection:
POST /api/v1/customers
Individual Resource
A specific resource is identified through its unique identifier.
GET /api/v1/customers/101
PATCH /api/v1/customers/101
DELETE /api/v1/customers/101
The identifier belongs in the path because it identifies the target resource.
Path Parameter vs Query Parameter
Use a path parameter to identify a resource.
GET /customers/101
Use query parameters to:
- Filter
- Sort
- Search
- Paginate
- Expand optional details
Example:
GET /customers?status=ACTIVE&page=0&size=20
| Requirement | Recommended Endpoint |
|---|---|
| Get customer 101 | /customers/101 |
| Get active customers | /customers?status=ACTIVE |
| Sort by name | /customers?sort=name,asc |
| Search by city | /customers?city=San%20Antonio |
| Paginate | /customers?page=0&size=20 |
Nested Resources
Resources may have parent-child relationships.
Example:
GET /api/v1/customers/101/accounts
Meaning:
Retrieve accounts that belong to customer 101
Specific account:
GET /api/v1/customers/101/accounts/501
Nested resources are useful when:
- The child belongs to the parent
- The parent relationship is important
- Authorization depends on the parent
- The child is normally accessed within that context
Avoid Deeply Nested URIs
Bad:
/customers/101/accounts/501/transactions/9001/disputes/3001/documents/7001
Problems:
- Difficult to read
- Difficult to maintain
- Strong coupling between resource paths
- Harder routing
- Long URLs
- Duplicate resource access patterns
Better:
/disputes/3001/documents/7001
or:
/documents/7001
depending on domain ownership.
A practical guideline is to keep nesting to one or two levels whenever possible.
Resource Representation
A resource and its representation are related but not identical.
Resource:
Customer 101
JSON representation:
{
"id": 101,
"name": "Venu",
"status": "ACTIVE"
}
XML representation:
<customer>
<id>101</id>
<name>Venu</name>
<status>ACTIVE</status>
</customer>
The resource is the business concept.
JSON and XML are representations of that concept.
Resource State
Resources usually have state.
Initial order:
{
"orderId": "ORD-1001",
"status": "CREATED",
"amount": 250.00
}
After payment:
{
"orderId": "ORD-1001",
"status": "PAID",
"amount": 250.00
}
After shipment:
{
"orderId": "ORD-1001",
"status": "SHIPPED",
"amount": 250.00
}
The resource identity remains the same while its state changes.
Modeling an Action as a State Update
Some business actions can be represented as state updates.
Example:
PATCH /api/v1/orders/ORD-1001
Content-Type: application/json
{
"status": "CANCELLED"
}
This approach is suitable when:
- The operation is a simple state change
- No additional business data is required
- The workflow is immediate
- A separate action history is unnecessary
- Authorization is straightforward
Limitations of Simple State Updates
Cancellation may require:
- Cancellation reason
- Requesting user
- Approval
- Refund processing
- Inventory release
- Audit history
- Notification
- Asynchronous processing
In that case, directly changing the status may hide important business behavior.
A cancellation resource may be more appropriate.
Modeling an Action as a Subresource
Instead of:
POST /api/v1/cancelOrder
use:
POST /api/v1/orders/ORD-1001/cancellations
Request:
{
"reasonCode": "CUSTOMER_REQUEST",
"comment": "Customer selected the wrong product."
}
Response:
HTTP/1.1 201 Created
Location: /api/v1/orders/ORD-1001/cancellations/CAN-501
{
"cancellationId": "CAN-501",
"orderId": "ORD-1001",
"reasonCode": "CUSTOMER_REQUEST",
"status": "ACCEPTED"
}
The cancellation becomes a resource with:
- Identity
- State
- Creation time
- Audit information
- Processing result
- Failure reason
Action Endpoint vs Action Resource
Action endpoint:
POST /orders/ORD-1001/cancel
Action resource:
POST /orders/ORD-1001/cancellations
State update:
PATCH /orders/ORD-1001
{
"status": "CANCELLED"
}
| Approach | Best Use |
|---|---|
| State update | Simple state transition |
| Action endpoint | Clear one-time business command |
| Action resource | Auditable operation with its own lifecycle |
When Verb-Based Endpoints Are Acceptable
Not every enterprise operation fits traditional CRUD.
Verb-based or command endpoints may be appropriate for:
/accounts/{id}/freeze
/passwords/reset
/payments/{id}/capture
/payments/{id}/refund
/tokens/revoke
/reports/generate
They are reasonable when:
- The operation has strong business meaning
- It triggers multiple side effects
- It requires distinct authorization
- It cannot be represented clearly as CRUD
- The operation has dedicated input
- Modeling it as a resource adds unnecessary complexity
Consistency and clarity are more important than forcing every operation into a resource model.
Resource-Oriented Money Transfer
Service-style endpoint:
POST /api/v1/transferMoney
Request:
{
"fromAccountId": "ACC-1001",
"toAccountId": "ACC-2001",
"amount": 500
}
Resource-oriented alternative:
POST /api/v1/transfers
Request:
{
"sourceAccountId": "ACC-1001",
"destinationAccountId": "ACC-2001",
"amount": 500,
"currency": "USD"
}
Response:
HTTP/1.1 201 Created
Location: /api/v1/transfers/TRF-9001
{
"transferId": "TRF-9001",
"status": "PROCESSING",
"amount": 500,
"currency": "USD"
}
Check transfer status:
GET /api/v1/transfers/TRF-9001
Why Transfer Is a Resource
A transfer has:
- A unique identifier
- Source account
- Destination account
- Amount
- Currency
- Status
- Creation timestamp
- Completion timestamp
- Failure reason
- Audit information
Possible lifecycle:
PENDING
↓
VALIDATING
↓
PROCESSING
↓
COMPLETED
Failure flow:
PROCESSING
↓
FAILED
Because a transfer has identity, state, and lifecycle, it is a natural resource.
Resource-Oriented Report Generation
Service-style:
POST /api/v1/generateReport
Resource-oriented:
POST /api/v1/report-jobs
Request:
{
"reportType": "MONTHLY_ACCOUNT_SUMMARY",
"customerId": "CUS-101",
"format": "PDF"
}
Response:
HTTP/1.1 202 Accepted
Location: /api/v1/report-jobs/JOB-1001
{
"jobId": "JOB-1001",
"status": "QUEUED"
}
Retrieve job status:
GET /api/v1/report-jobs/JOB-1001
Completed response:
{
"jobId": "JOB-1001",
"status": "COMPLETED",
"reportId": "RPT-9001",
"downloadUrl": "/api/v1/reports/RPT-9001"
}
Resource-Oriented Claim Approval
Service-style:
POST /api/v1/approveClaim
Option 1: State Update
PATCH /api/v1/claims/CLM-1001
{
"status": "APPROVED",
"approvedAmount": 2500
}
Option 2: Approval Resource
POST /api/v1/claims/CLM-1001/approvals
{
"approvedAmount": 2500,
"reasonCode": "DOCUMENTS_VERIFIED"
}
The approval resource is useful when approval requires:
- Approver identity
- Approval timestamp
- Approval comments
- Multiple approval levels
- Reversal support
- Audit history
Resource Design Decision Framework
Does the business concept have identity or state?
├── Yes
│ ↓
│ Model it as a resource
│
└── No
↓
Is it a simple state transition?
├── Yes
│ ↓
│ Consider PUT or PATCH
│
└── No
↓
Does the operation need lifecycle or audit tracking?
├── Yes
│ ↓
│ Model it as an action resource or subresource
│
└── No
↓
Use a clearly named command endpoint
Resource Granularity
Resources should represent business concepts, not database tables.
Bad:
/customer_master_table
/customer_address_table
/customer_status_table
Better:
/customers
/customers/{id}/addresses
The API should expose the business domain rather than physical database design.
Aggregate Resources
A business resource may combine data from several internal tables or services.
Example:
{
"orderId": "ORD-1001",
"customer": {
"customerId": "CUS-101",
"name": "Venu"
},
"items": [
{
"productId": "PRD-501",
"quantity": 2,
"price": 125.00
}
],
"shippingAddress": {
"city": "San Antonio",
"state": "TX"
},
"status": "CREATED",
"totalAmount": 250.00
}
The consumer sees one order resource even when data comes from:
- Orders table
- Order items table
- Customer service
- Address table
- Product service
Resource Ownership in Microservices
Each microservice should own its business resources.
Customer Service
Owns
Customers
Order Service
Owns
Orders
Payment Service
Owns
Payments
Inventory Service
Owns
Inventory
A service should not directly modify another service's database.
Bad:
Order Service
↓
Payment Database
Correct:
Order Service
↓
Payment API
↓
Payment Service
↓
Payment Database
Resource URI Does Not Expose Storage
A resource URI identifies a business concept.
It should not reveal:
- Table names
- Database names
- Internal server addresses
- Storage technology
- Java class names
- Infrastructure layout
Example:
/api/v1/customers/101
The customer could be stored in:
- PostgreSQL
- MongoDB
- Mainframe
- External CRM
- Distributed cache
- Multiple systems
The consumer should not need to know.
Complete Spring Boot Example
We will create a resource-oriented order API supporting:
- Order creation
- Order retrieval
- Order status update
- Cancellation creation
- Cancellation retrieval
- Validation
- Standardized error handling
API Endpoints
| Operation | Method | Endpoint |
|---|---|---|
| Create order | POST | /api/v1/orders |
| Get order | GET | /api/v1/orders/{orderId} |
| Update order status | PATCH | /api/v1/orders/{orderId} |
| Create cancellation | POST | /api/v1/orders/{orderId}/cancellations |
| Get cancellation | GET | /api/v1/orders/{orderId}/cancellations/{cancellationId} |
Project Structure
src/main/java/com/codewithvenu/resourceapi
├── controller
│ └── OrderController.java
├── dto
│ ├── OrderStatus.java
│ ├── CreateOrderRequest.java
│ ├── UpdateOrderRequest.java
│ ├── CreateCancellationRequest.java
│ ├── OrderResponse.java
│ ├── CancellationResponse.java
│ └── ApiErrorResponse.java
├── exception
│ ├── OrderNotFoundException.java
│ ├── CancellationNotFoundException.java
│ ├── InvalidOrderStateException.java
│ └── GlobalExceptionHandler.java
├── service
│ └── OrderService.java
└── ResourceApiApplication.java
Order Status Enum
package com.codewithvenu.resourceapi.dto;
public enum OrderStatus {
CREATED,
CONFIRMED,
PAID,
SHIPPED,
DELIVERED,
CANCELLED
}
Create Order Request
package com.codewithvenu.resourceapi.dto;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
public record CreateOrderRequest(
@NotBlank(message = "Customer ID is required")
String customerId,
@NotNull(message = "Amount is required")
@DecimalMin(
value = "0.01",
message = "Amount must be greater than zero"
)
BigDecimal amount,
@NotBlank(message = "Currency is required")
String currency
) {
}
Update Order Request
package com.codewithvenu.resourceapi.dto;
import jakarta.validation.constraints.NotNull;
public record UpdateOrderRequest(
@NotNull(message = "Order status is required")
OrderStatus status
) {
}
Create Cancellation Request
package com.codewithvenu.resourceapi.dto;
import jakarta.validation.constraints.NotBlank;
public record CreateCancellationRequest(
@NotBlank(message = "Reason code is required")
String reasonCode,
String comment
) {
}
Order Response
package com.codewithvenu.resourceapi.dto;
import java.math.BigDecimal;
import java.time.Instant;
public record OrderResponse(
String orderId,
String customerId,
BigDecimal amount,
String currency,
OrderStatus status,
Instant createdAt,
Instant updatedAt
) {
}
Cancellation Response
package com.codewithvenu.resourceapi.dto;
import java.time.Instant;
public record CancellationResponse(
String cancellationId,
String orderId,
String reasonCode,
String comment,
String status,
Instant createdAt
) {
}
Order Not Found Exception
package com.codewithvenu.resourceapi.exception;
public class OrderNotFoundException
extends RuntimeException {
public OrderNotFoundException(String orderId) {
super("Order " + orderId + " was not found.");
}
}
Cancellation Not Found Exception
package com.codewithvenu.resourceapi.exception;
public class CancellationNotFoundException
extends RuntimeException {
public CancellationNotFoundException(
String cancellationId
) {
super(
"Cancellation "
+ cancellationId
+ " was not found."
);
}
}
Invalid Order State Exception
package com.codewithvenu.resourceapi.exception;
public class InvalidOrderStateException
extends RuntimeException {
public InvalidOrderStateException(
String message
) {
super(message);
}
}
Order Service
package com.codewithvenu.resourceapi.service;
import com.codewithvenu.resourceapi.dto.CancellationResponse;
import com.codewithvenu.resourceapi.dto.CreateCancellationRequest;
import com.codewithvenu.resourceapi.dto.CreateOrderRequest;
import com.codewithvenu.resourceapi.dto.OrderResponse;
import com.codewithvenu.resourceapi.dto.OrderStatus;
import com.codewithvenu.resourceapi.dto.UpdateOrderRequest;
import com.codewithvenu.resourceapi.exception.CancellationNotFoundException;
import com.codewithvenu.resourceapi.exception.InvalidOrderStateException;
import com.codewithvenu.resourceapi.exception.OrderNotFoundException;
import java.time.Instant;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final Map<String, OrderResponse> orders =
new ConcurrentHashMap<>();
private final Map<String, CancellationResponse>
cancellations =
new ConcurrentHashMap<>();
public OrderResponse createOrder(
CreateOrderRequest request
) {
String orderId =
"ORD-" + UUID.randomUUID()
.toString()
.substring(0, 8)
.toUpperCase();
Instant currentTime = Instant.now();
OrderResponse order =
new OrderResponse(
orderId,
request.customerId(),
request.amount(),
request.currency(),
OrderStatus.CREATED,
currentTime,
currentTime
);
orders.put(orderId, order);
return order;
}
public OrderResponse getOrder(
String orderId
) {
OrderResponse order =
orders.get(orderId);
if (order == null) {
throw new OrderNotFoundException(
orderId
);
}
return order;
}
public OrderResponse updateOrder(
String orderId,
UpdateOrderRequest request
) {
OrderResponse currentOrder =
getOrder(orderId);
validateStateTransition(
currentOrder.status(),
request.status()
);
OrderResponse updatedOrder =
new OrderResponse(
currentOrder.orderId(),
currentOrder.customerId(),
currentOrder.amount(),
currentOrder.currency(),
request.status(),
currentOrder.createdAt(),
Instant.now()
);
orders.put(
orderId,
updatedOrder
);
return updatedOrder;
}
public CancellationResponse createCancellation(
String orderId,
CreateCancellationRequest request
) {
OrderResponse currentOrder =
getOrder(orderId);
if (
currentOrder.status()
== OrderStatus.DELIVERED
) {
throw new InvalidOrderStateException(
"Delivered orders cannot be cancelled."
);
}
if (
currentOrder.status()
== OrderStatus.CANCELLED
) {
throw new InvalidOrderStateException(
"Order is already cancelled."
);
}
String cancellationId =
"CAN-" + UUID.randomUUID()
.toString()
.substring(0, 8)
.toUpperCase();
CancellationResponse cancellation =
new CancellationResponse(
cancellationId,
orderId,
request.reasonCode(),
request.comment(),
"ACCEPTED",
Instant.now()
);
cancellations.put(
cancellationKey(
orderId,
cancellationId
),
cancellation
);
OrderResponse cancelledOrder =
new OrderResponse(
currentOrder.orderId(),
currentOrder.customerId(),
currentOrder.amount(),
currentOrder.currency(),
OrderStatus.CANCELLED,
currentOrder.createdAt(),
Instant.now()
);
orders.put(
orderId,
cancelledOrder
);
return cancellation;
}
public CancellationResponse getCancellation(
String orderId,
String cancellationId
) {
getOrder(orderId);
CancellationResponse cancellation =
cancellations.get(
cancellationKey(
orderId,
cancellationId
)
);
if (cancellation == null) {
throw new CancellationNotFoundException(
cancellationId
);
}
return cancellation;
}
private void validateStateTransition(
OrderStatus currentStatus,
OrderStatus requestedStatus
) {
boolean validTransition =
switch (currentStatus) {
case CREATED ->
requestedStatus
== OrderStatus.CONFIRMED
|| requestedStatus
== OrderStatus.CANCELLED;
case CONFIRMED ->
requestedStatus
== OrderStatus.PAID
|| requestedStatus
== OrderStatus.CANCELLED;
case PAID ->
requestedStatus
== OrderStatus.SHIPPED
|| requestedStatus
== OrderStatus.CANCELLED;
case SHIPPED ->
requestedStatus
== OrderStatus.DELIVERED;
case DELIVERED, CANCELLED ->
false;
};
if (!validTransition) {
throw new InvalidOrderStateException(
"Order cannot transition from "
+ currentStatus
+ " to "
+ requestedStatus
+ "."
);
}
}
private String cancellationKey(
String orderId,
String cancellationId
) {
return orderId + ":" + cancellationId;
}
}
Order Controller
package com.codewithvenu.resourceapi.controller;
import com.codewithvenu.resourceapi.dto.CancellationResponse;
import com.codewithvenu.resourceapi.dto.CreateCancellationRequest;
import com.codewithvenu.resourceapi.dto.CreateOrderRequest;
import com.codewithvenu.resourceapi.dto.OrderResponse;
import com.codewithvenu.resourceapi.dto.UpdateOrderRequest;
import com.codewithvenu.resourceapi.service.OrderService;
import jakarta.validation.Valid;
import java.net.URI;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(
OrderService orderService
) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<OrderResponse>
createOrder(
@Valid
@RequestBody
CreateOrderRequest request
) {
OrderResponse response =
orderService.createOrder(
request
);
URI location =
URI.create(
"/api/v1/orders/"
+ response.orderId()
);
return ResponseEntity
.created(location)
.body(response);
}
@GetMapping("/{orderId}")
public ResponseEntity<OrderResponse>
getOrder(
@PathVariable String orderId
) {
OrderResponse response =
orderService.getOrder(
orderId
);
return ResponseEntity.ok(response);
}
@PatchMapping("/{orderId}")
public ResponseEntity<OrderResponse>
updateOrder(
@PathVariable String orderId,
@Valid
@RequestBody
UpdateOrderRequest request
) {
OrderResponse response =
orderService.updateOrder(
orderId,
request
);
return ResponseEntity.ok(response);
}
@PostMapping(
"/{orderId}/cancellations"
)
public ResponseEntity<CancellationResponse>
createCancellation(
@PathVariable String orderId,
@Valid
@RequestBody
CreateCancellationRequest request
) {
CancellationResponse response =
orderService.createCancellation(
orderId,
request
);
URI location =
URI.create(
"/api/v1/orders/"
+ orderId
+ "/cancellations/"
+ response.cancellationId()
);
return ResponseEntity
.created(location)
.body(response);
}
@GetMapping(
"/{orderId}/cancellations/{cancellationId}"
)
public ResponseEntity<CancellationResponse>
getCancellation(
@PathVariable String orderId,
@PathVariable String cancellationId
) {
CancellationResponse response =
orderService.getCancellation(
orderId,
cancellationId
);
return ResponseEntity.ok(response);
}
}
API Error Response
package com.codewithvenu.resourceapi.dto;
import java.time.Instant;
import java.util.List;
public record ApiErrorResponse(
Instant timestamp,
int status,
String code,
String message,
String path,
List<String> validationErrors
) {
}
Global Exception Handler
package com.codewithvenu.resourceapi.exception;
import com.codewithvenu.resourceapi.dto.ApiErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
import java.time.Instant;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<ApiErrorResponse>
handleOrderNotFound(
OrderNotFoundException exception,
HttpServletRequest request
) {
return buildErrorResponse(
HttpStatus.NOT_FOUND,
"ORDER_NOT_FOUND",
exception.getMessage(),
request.getRequestURI(),
List.of()
);
}
@ExceptionHandler(
CancellationNotFoundException.class
)
public ResponseEntity<ApiErrorResponse>
handleCancellationNotFound(
CancellationNotFoundException exception,
HttpServletRequest request
) {
return buildErrorResponse(
HttpStatus.NOT_FOUND,
"CANCELLATION_NOT_FOUND",
exception.getMessage(),
request.getRequestURI(),
List.of()
);
}
@ExceptionHandler(
InvalidOrderStateException.class
)
public ResponseEntity<ApiErrorResponse>
handleInvalidOrderState(
InvalidOrderStateException exception,
HttpServletRequest request
) {
return buildErrorResponse(
HttpStatus.CONFLICT,
"INVALID_ORDER_STATE",
exception.getMessage(),
request.getRequestURI(),
List.of()
);
}
@ExceptionHandler(
MethodArgumentNotValidException.class
)
public ResponseEntity<ApiErrorResponse>
handleValidationFailure(
MethodArgumentNotValidException exception,
HttpServletRequest request
) {
List<String> validationErrors =
exception
.getBindingResult()
.getFieldErrors()
.stream()
.map(
error ->
error.getField()
+ ": "
+ error.getDefaultMessage()
)
.toList();
return buildErrorResponse(
HttpStatus.BAD_REQUEST,
"VALIDATION_FAILED",
"Request validation failed.",
request.getRequestURI(),
validationErrors
);
}
private ResponseEntity<ApiErrorResponse>
buildErrorResponse(
HttpStatus status,
String code,
String message,
String path,
List<String> validationErrors
) {
ApiErrorResponse response =
new ApiErrorResponse(
Instant.now(),
status.value(),
code,
message,
path,
validationErrors
);
return ResponseEntity
.status(status)
.body(response);
}
}
Create Order Request
POST /api/v1/orders
Content-Type: application/json
{
"customerId": "CUS-101",
"amount": 250.00,
"currency": "USD"
}
Response:
HTTP/1.1 201 Created
Location: /api/v1/orders/ORD-A8C12F40
Content-Type: application/json
{
"orderId": "ORD-A8C12F40",
"customerId": "CUS-101",
"amount": 250.00,
"currency": "USD",
"status": "CREATED",
"createdAt": "2026-07-19T15:00:00Z",
"updatedAt": "2026-07-19T15:00:00Z"
}
Retrieve Order
Request:
GET /api/v1/orders/ORD-A8C12F40
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"orderId": "ORD-A8C12F40",
"customerId": "CUS-101",
"amount": 250.00,
"currency": "USD",
"status": "CREATED",
"createdAt": "2026-07-19T15:00:00Z",
"updatedAt": "2026-07-19T15:00:00Z"
}
Update Order State
Request:
PATCH /api/v1/orders/ORD-A8C12F40
Content-Type: application/json
{
"status": "CONFIRMED"
}
Response:
HTTP/1.1 200 OK
{
"orderId": "ORD-A8C12F40",
"customerId": "CUS-101",
"amount": 250.00,
"currency": "USD",
"status": "CONFIRMED",
"createdAt": "2026-07-19T15:00:00Z",
"updatedAt": "2026-07-19T15:05:00Z"
}
Create Cancellation Resource
Request:
POST /api/v1/orders/ORD-A8C12F40/cancellations
Content-Type: application/json
{
"reasonCode": "CUSTOMER_REQUEST",
"comment": "Customer ordered the wrong item."
}
Response:
HTTP/1.1 201 Created
Location: /api/v1/orders/ORD-A8C12F40/cancellations/CAN-B91F4201
{
"cancellationId": "CAN-B91F4201",
"orderId": "ORD-A8C12F40",
"reasonCode": "CUSTOMER_REQUEST",
"comment": "Customer ordered the wrong item.",
"status": "ACCEPTED",
"createdAt": "2026-07-19T15:10:00Z"
}
Invalid State Transition
Request:
PATCH /api/v1/orders/ORD-A8C12F40
Content-Type: application/json
{
"status": "DELIVERED"
}
Response:
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"timestamp": "2026-07-19T15:15:00Z",
"status": 409,
"code": "INVALID_ORDER_STATE",
"message": "Order cannot transition from CREATED to DELIVERED.",
"path": "/api/v1/orders/ORD-A8C12F40",
"validationErrors": []
}
Step-by-Step Request Flow
For:
POST /api/v1/orders
the request flows through:
Client
↓
API Gateway
↓
DispatcherServlet
↓
OrderController
↓
Request Validation
↓
OrderService
↓
Business Rules
↓
Repository or Database
↓
OrderResponse DTO
↓
Jackson Serialization
↓
HTTP 201 Response
↓
Client
Why DTOs Are Important
Do not expose database entities directly.
Bad:
@GetMapping("/{orderId}")
public OrderEntity getOrder(
@PathVariable String orderId
) {
return orderRepository
.findById(orderId)
.orElseThrow();
}
Problems:
- Database structure becomes part of the API contract
- Sensitive columns may be exposed
- Lazy-loading problems may occur
- Internal changes can break consumers
- Persistence annotations leak into API design
Better:
@GetMapping("/{orderId}")
public OrderResponse getOrder(
@PathVariable String orderId
) {
return orderService.getOrder(orderId);
}
Resource-Oriented Design and Idempotency
HTTP semantics help communicate retry behavior.
| Operation | Typical Idempotency |
|---|---|
| GET resource | Idempotent |
| PUT resource | Idempotent |
| DELETE resource | Idempotent |
| POST collection | Not idempotent by default |
| PATCH resource | Depends on operation |
Example:
PUT /customers/101
Sending the same representation repeatedly should produce the same final resource state.
Example:
POST /transfers
Repeated requests may create duplicate transfers unless an idempotency key is used.
Resource-Oriented Design and Caching
Resource retrieval works naturally with HTTP caching.
Request:
GET /api/v1/products/501
Response:
HTTP/1.1 200 OK
ETag: "product-v7"
Cache-Control: public, max-age=300
Later request:
GET /api/v1/products/501
If-None-Match: "product-v7"
Response:
HTTP/1.1 304 Not Modified
A generic service endpoint such as:
POST /api/v1/getProduct
is less naturally cacheable.
Resource-Oriented Design and Authorization
Authorization can be aligned with resources.
Examples:
Read customer 101
Update customer 101
Cancel order ORD-1001
Approve claim CLM-1001
Possible access-control rule:
User may access an account only when:
account.customerId == authenticatedUser.customerId
Resource-based authorization often produces clearer policies.
Resource-Oriented Design and Events
Resources can generate domain events.
Example:
POST /orders
↓
Order Created
↓
OrderCreated Event
Cancellation flow:
POST /orders/{id}/cancellations
↓
Cancellation Created
↓
OrderCancellationRequested Event
Kafka consumers may include:
- Inventory service
- Payment service
- Notification service
- Audit service
Synchronous Resource Flow
Client
↓
POST /orders
↓
Order Service
↓
Database
↓
201 Created
The client waits for processing to finish.
Asynchronous Resource Flow
Client
↓
POST /report-jobs
↓
202 Accepted
↓
Kafka
↓
Report Worker
↓
Report Generated
The client checks:
GET /report-jobs/{jobId}
This is an effective model for long-running work.
Service-Oriented APIs in Enterprise Systems
Service-oriented operations may still be appropriate when exposing:
- Legacy SOAP capabilities
- Mainframe transactions
- Calculation engines
- Validation engines
- Workflow commands
- Business rule engines
- Stateless transformations
- Specialized financial operations
Example:
POST /api/v1/premium-calculations
This is still resource-oriented because the calculation itself is modeled as a noun.
However, a command such as:
POST /api/v1/calculate-premium
may also be acceptable when it is simpler and consistently documented.
RPC-Style APIs
RPC-style APIs expose functions.
Examples:
/customerService/getCustomer
/orderService/createOrder
/paymentService/capturePayment
Benefits:
- Direct mapping to business methods
- Easy for command-heavy operations
- Familiar to developers
- Can be efficient for internal systems
Drawbacks:
- Weaker use of HTTP semantics
- Every operation may use POST
- Harder caching
- Less predictable URIs
- Larger number of custom endpoints
- Retry behavior may be unclear
Resource vs Service in gRPC
gRPC naturally follows a service and method model.
Example:
service PaymentService {
rpc CreatePayment(
CreatePaymentRequest
) returns (
PaymentResponse
);
rpc CapturePayment(
CapturePaymentRequest
) returns (
PaymentResponse
);
}
REST typically emphasizes resources.
gRPC emphasizes service methods.
Neither is automatically better.
The right choice depends on:
- Consumer type
- Performance requirements
- Internal or external use
- Browser support
- Streaming needs
- Contract tooling
- Organizational standards
Common Design Mistakes
1. Using Verbs for Every Endpoint
Bad:
/getCustomer
/createCustomer
/updateCustomer
/deleteCustomer
Better:
GET /customers/{id}
POST /customers
PATCH /customers/{id}
DELETE /customers/{id}
2. Treating Database Tables as Resources
Bad:
/customer_master
/customer_status_lookup
/order_transaction_table
The API should represent business capabilities, not physical storage.
3. Exposing Every Service Method as an Endpoint
Internal method:
recalculateCustomerRiskScore()
It does not automatically require:
POST /recalculateCustomerRiskScore
First determine:
- Is it a consumer-facing capability?
- Is it part of another workflow?
- Does it require its own authorization?
- Does it have a lifecycle?
- Can it be modeled as a resource?
4. Deeply Nested Resources
Avoid paths that contain too many levels.
Deep nesting increases coupling and reduces usability.
5. Using POST for All Operations
Using POST for retrieval, updates, and deletion loses:
- HTTP semantics
- Natural caching
- Retry expectations
- Standard status behavior
- API predictability
6. Using PATCH for Complex Commands
Bad:
PATCH /payments/PAY-101
{
"action": "CAPTURE_AND_NOTIFY_AND_SETTLE"
}
A dedicated action or command endpoint may be clearer.
7. Allowing Invalid State Transitions
An order should not move directly from:
CREATED
↓
DELIVERED
Validate transitions in the service layer.
8. Returning Internal Entities
Always separate:
Database Entity
from
API Representation
Use DTOs.
9. Ignoring Resource Ownership
A microservice should not directly update another service's resources or database.
10. Forcing Every Action Into CRUD
Overly strict REST modeling can make APIs confusing.
A clear action endpoint is better than an artificial or misleading resource.
Best Practices
- Use nouns for primary resources.
- Use plural names for collections.
- Use HTTP methods according to their semantics.
- Keep URI naming consistent.
- Use path parameters for resource identity.
- Use query parameters for filtering, sorting, searching, and pagination.
- Avoid exposing database table names.
- Avoid deeply nested resource paths.
- Model long-running processes as job resources.
- Model auditable actions as subresources.
- Use PATCH for simple partial state changes.
- Use action endpoints when they clearly represent complex commands.
- Use DTOs instead of exposing entities.
- Validate state transitions in the service layer.
- Document retry and idempotency behavior.
- Return accurate HTTP status codes.
- Use
201 Createdand aLocationheader for new resources. - Use
202 Acceptedfor asynchronous processing. - Use
409 Conflictfor invalid state transitions. - Define clear resource ownership in microservices.
- Protect resources with resource-level authorization.
- Maintain backward compatibility.
- Use consistent error-response models.
- Document resource lifecycles.
- Keep consumers independent of storage details.
Production Scenario 1: Order Cancellation
Requirement:
A customer wants to cancel an order.
Possible approaches:
Simple Update
PATCH /orders/ORD-1001
{
"status": "CANCELLED"
}
Use when cancellation is immediate and simple.
Action Endpoint
POST /orders/ORD-1001/cancel
Use when cancellation is a distinct command.
Cancellation Resource
POST /orders/ORD-1001/cancellations
Use when cancellation requires:
- Audit history
- Approval
- Reason code
- Refund processing
- Status tracking
Production Scenario 2: Password Reset
Password reset does not fit normal CRUD well.
Possible design:
POST /password-reset-requests
Request:
{
"email": "[email protected]"
}
Response:
HTTP/1.1 202 Accepted
The reset request becomes a resource or process.
Complete reset:
POST /password-resets
{
"token": "reset-token",
"newPassword": "new-secure-password"
}
Production Scenario 3: Payment Capture
Payment resource:
GET /payments/PAY-101
Capture command:
POST /payments/PAY-101/captures
Request:
{
"amount": 250.00
}
The capture becomes an auditable financial resource.
Production Scenario 4: Address Validation
This may be modeled as a service-like calculation.
POST /address-validations
Request:
{
"addressLine1": "123 Main Street",
"city": "San Antonio",
"state": "TX",
"postalCode": "78240"
}
Response:
{
"valid": true,
"standardizedAddress": {
"addressLine1": "123 MAIN ST",
"city": "SAN ANTONIO",
"state": "TX",
"postalCode": "78240"
}
}
The validation result is modeled as a resource representation.
Production Scenario 5: Long-Running Export
Bad design:
GET /customers/export
The request takes several minutes and may time out.
Better:
POST /export-jobs
Response:
HTTP/1.1 202 Accepted
Location: /export-jobs/JOB-101
Check status:
GET /export-jobs/JOB-101
Download result:
GET /exports/EXP-501
Frequently Asked Interview Questions
1. What is a resource in REST?
A resource is a business entity, concept, collection, event, or process that can be identified using a URI.
Examples include:
- Customer
- Account
- Order
- Payment
- Transfer
- Report job
A resource normally has identity, state, representation, and lifecycle.
2. What is a service-oriented API?
A service-oriented API exposes business operations or capabilities directly.
Example:
POST /calculatePremium
The endpoint focuses on executing a function rather than managing a resource.
3. What is the main difference between a resource and a service?
A resource represents a noun, entity, or stateful business concept.
A service represents an operation or capability.
Example:
Resource
/payment-transfers/TRF-101
Service
/transferMoney
4. Why does REST prefer nouns in URIs?
REST uses the URI to identify the resource and the HTTP method to communicate the operation.
Example:
GET /customers/101
customers/101 identifies the target, while GET describes the action.
5. Should verbs never be used in REST endpoints?
No.
Verbs may be appropriate for complex commands that do not map clearly to CRUD operations.
Examples:
/payments/{id}/capture
/accounts/{id}/freeze
/tokens/revoke
Clarity and consistency are more important than forcing every action into a resource model.
6. How do you model a business action in REST?
A business action can be modeled as:
- A state update using PUT or PATCH
- A command endpoint
- An action subresource
Example cancellation resource:
POST /orders/{id}/cancellations
The best choice depends on complexity, lifecycle, audit requirements, and business meaning.
7. When should an action be modeled as a subresource?
Use a subresource when the action has:
- Identity
- State
- Audit history
- Processing status
- Approval
- Failure details
- Its own lifecycle
Example:
POST /claims/{claimId}/approvals
8. How should path and query parameters be used?
Use path parameters to identify a resource.
GET /customers/101
Use query parameters to filter, sort, search, or paginate collections.
GET /customers?status=ACTIVE&page=0
9. What is the difference between a resource and its representation?
A resource is the conceptual business object.
A representation is the format used to describe it, such as JSON or XML.
The same resource may have multiple representations.
10. How do resource-oriented APIs improve caching?
Resource retrieval normally uses GET and stable URIs.
This allows standard HTTP caching through:
- Cache-Control
- ETag
- If-None-Match
- Last-Modified
Command-style POST endpoints are generally harder to cache.
11. How do resource-oriented APIs support idempotency?
HTTP methods communicate retry semantics.
GET, PUT, and DELETE are expected to be idempotent.
POST is not idempotent by default and may require an idempotency key for operations such as payments and transfers.
12. How should long-running operations be modeled?
Model them as job resources.
Example:
POST /report-jobs
Return:
202 Accepted
Location: /report-jobs/JOB-101
The client can then retrieve the job status using GET.
13. Why should database tables not be exposed as API resources?
Database tables are implementation details.
Exposing them creates tight coupling between:
- API consumers
- Database schema
- Storage technology
APIs should expose business-domain concepts instead.
14. How does resource ownership work in microservices?
Each microservice owns its resources and persistence.
For example:
- Customer service owns customers
- Order service owns orders
- Payment service owns payments
Other services should interact through APIs or events rather than accessing databases directly.
15. How do you decide between resource-oriented and service-oriented design?
Use resource-oriented design when the concept has:
- Identity
- State
- Representation
- Lifecycle
Use PUT or PATCH for simple state changes.
Use an action resource when the operation needs tracking or audit history.
Use a command-style endpoint when the operation is complex and cannot be modeled clearly as a resource.
The final design should prioritize clarity, consistency, HTTP semantics, and consumer usability.
Senior-Level Interview Answer
A strong senior-level answer could be:
Resource-oriented APIs expose business entities, processes, and state through noun-based URIs, while HTTP methods communicate the operation. For example, I would use
POST /transfersinstead ofPOST /transferMoneybecause a transfer has identity, status, lifecycle, and audit requirements. For simple state transitions, I may use PATCH. For complex or auditable actions, I usually model the action as a subresource, such asPOST /orders/{id}/cancellations. However, I do not force every business command into CRUD. Operations such as payment capture or token revocation may use explicit action endpoints when that produces a clearer and more maintainable contract.
Quick Revision
| Topic | Summary |
|---|---|
| Resource | Business entity, concept, event, or process |
| Service | Business operation or capability |
| Resource URI | Usually noun-based |
| Service URI | Often verb- or command-based |
| Collection | /customers |
| Individual resource | /customers/101 |
| Path parameter | Identifies a resource |
| Query parameter | Filters or modifies collection retrieval |
| Simple state change | PUT or PATCH |
| Auditable action | Action subresource |
| Long-running work | Job resource with 202 |
| Resource creation | POST collection with 201 |
| Invalid state transition | 409 Conflict |
| Microservice rule | Each service owns its resources |
| Interview Frequency | ⭐⭐⭐⭐⭐ |
Key Takeaways
- A resource represents a business entity, process, event, or stateful concept.
- A service represents a business operation or capability.
- REST APIs normally use noun-based URIs and HTTP methods.
- Resource-oriented APIs are generally more predictable and easier to cache.
- Path parameters identify resources, while query parameters filter or paginate collections.
- Simple state transitions can be modeled with PUT or PATCH.
- Complex and auditable actions can be modeled as subresources.
- Long-running operations should be modeled as job resources.
- Verb-based commands are acceptable when they provide clearer business meaning.
- APIs should expose business-domain concepts rather than database tables.
- Each microservice should own its resources and persistence.
- DTOs should separate API representations from database entities.
- Resource state transitions must be validated in the service layer.
- Good API design balances REST principles with business clarity.
- The best design is consistent, understandable, secure, maintainable, and easy for consumers to use.