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 Created and a Location header for new resources.
  • Use 202 Accepted for asynchronous processing.
  • Use 409 Conflict for 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:

  1. A state update using PUT or PATCH
  2. A command endpoint
  3. 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 /transfers instead of POST /transferMoney because 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 as POST /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.