DEV Community

Cover image for Production-Grade Spring Boot APIs — Part 3: Production-Grade Folder Structure & Layered Implementation
Pratik280
Pratik280

Posted on • Edited on

Production-Grade Spring Boot APIs — Part 3: Production-Grade Folder Structure & Layered Implementation

A practical guide to production-grade Spring Boot folder structure and layered architecture, covering controllers, services, repositories, DTOs, entities, BaseResponse usage, and global exception handling.

This is Part 3 of the Production-Grade Spring Boot API Design series.

Code Gihub Repo: https://github.com/Pratik280/order-system/tree/develop

In production Spring Boot applications, folder structure is not cosmetic.

It enforces separation of concerns, improves readability, and allows teams to scale safely.

Below is a typical production-ready package structure used for REST APIs.

com.example.orderservice
│
├── controller
│ └── OrderController.java
│
├── entity
│ └── OrderEntity.java
│
├── dto
│ ├── OrderRequest.java
│ └── OrderResponse.java
│
├── repository
│ └── OrderRepository.java
│
├── service
│ ├── OrderService.java
│ └── impl
│ └── OrderServiceImpl.java
│
├── exception
│ ├── BusinessException.java
│ └── ResourceNotFoundException.java
│
├── common
│ ├── BaseResponse.java
│ └── ResponseBuilder.java
│
└── handler
└── GlobalExceptionHandler.java
Enter fullscreen mode Exit fullscreen mode

Each layer has one clear responsibility and does not leak concerns into other layers.


Controller Layer — API Entry Point

@RestController
@RequestMapping("/orders")
public class OrderController {

    private OrderService orderService;

    public OrderController(OrderService orderService){
        this.orderService = orderService;
    }

    @GetMapping
    public ResponseEntity<BaseResponse<List<OrderResponse>>> getAllOrders(){
        return ResponseBuilder.success(
                HttpStatus.OK,
                "All orders fetch successfully",
                orderService.getAllOrders()
        );
    }

    @GetMapping("/{id}")
    public ResponseEntity<BaseResponse<OrderResponse>> getOrderById(@PathVariable Long id){
        return ResponseBuilder.success(
                HttpStatus.OK,
                "Order fetch successfully",
                orderService.getOrderById(id)
        );
    }

    @PostMapping
    public ResponseEntity<BaseResponse<OrderResponse>> createOrder(
            @Valid @RequestBody OrderRequest orderRequest){
        return ResponseBuilder.success(
                HttpStatus.OK,
                "Order created successfully",
                orderService.createOrder(orderRequest)
        );
    }
Enter fullscreen mode Exit fullscreen mode

Annotations Explained

  • @RestController

Marks the class as a REST API controller and automatically serializes responses to JSON.

  • @RequestMapping("/orders")

Defines a base URL for all endpoints in this controller.

  • @GetMapping

Handles HTTP GET requests.

  • @PostMapping

Handles HTTP POST requests.

  • @PathVariable

Extracts values from the URL path.

  • @RequestBody

Converts incoming JSON into a Java object.

Triggers validation on request DTOs.

Controllers do not contain business logic. They only orchestrate requests and responses.


Entity Layer — Database Representation

@Entity
@Table(name = "orders")
@Getter
@Setter
@NoArgsConstructor
public class OrderEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String productName;
}
Enter fullscreen mode Exit fullscreen mode

Entities:

  • Represent database tables
  • Are used only in the persistence layer
  • Are never exposed directly to API clients

DTO Layer — API Contracts

@Getter
@Setter
@NoArgsConstructor
public class OrderRequest {
    @NotBlank(message = "productName is mandatory")
    private String productName;
}
Enter fullscreen mode Exit fullscreen mode

@Getter
@Setter
@NoArgsConstructor
public class OrderResponse {
    private Long id;
    private String productName;
}
Enter fullscreen mode Exit fullscreen mode

DTOs:

  • Protect internal entity structure
  • Allow validation rules
  • Decouple API contracts from database design

Repository Layer — Data Access

public interface OrderRepository
        extends JpaRepository<OrderEntity, Long> { }
Enter fullscreen mode Exit fullscreen mode

Repositories:

  • Abstract database access
  • Provide CRUD operations automatically
  • Remove the need for DAOs and JDBC code

Service Layer — Business Logic Contract

public interface OrderService {
    List<OrderResponse> getAllOrders();
    OrderResponse getOrderById(Long id);
    OrderResponse createOrder(OrderRequest orderRequest);
}
Enter fullscreen mode Exit fullscreen mode

The service interface defines what the system can do, not how.


Service Implementation — Business Logic Execution

@Service
public class OrderServiceImpl implements OrderService {

    private OrderRepository orderRepository;
    private ModelMapper modelMapper;

    public OrderServiceImpl(OrderRepository orderRepository,
                            ModelMapper modelMapper){
        this.orderRepository = orderRepository;
        this.modelMapper = modelMapper;
    }

    @Override
    public List<OrderResponse> getAllOrders(){
        List<OrderResponse> orders = orderRepository.findAll()
                .stream()
                .map(orderEntity ->
                        modelMapper.map(orderEntity, OrderResponse.class))
                .toList();

        if(orders.isEmpty()){
            throw new ResourceNotFoundException("No orders found");
        }
        return orders;
    }

    @Override
    public OrderResponse getOrderById(Long id){
        return orderRepository.findById(id)
                .map(orderEntity ->
                        modelMapper.map(orderEntity, OrderResponse.class))
                .orElseThrow(() ->
                        new ResourceNotFoundException(
                                "Order not found with id " + id));
    }

    @Override
    public OrderResponse createOrder(OrderRequest orderRequest) {

        OrderEntity orderEntity;
        try{
            orderEntity = modelMapper.map(orderRequest, OrderEntity.class);
        } catch (Exception e){
            throw new BusinessException(
                    "MAPPING_ERROR",
                    "Failed to map order request");
        }

        try{
            OrderEntity saved = orderRepository.save(orderEntity);
            return modelMapper.map(saved, OrderResponse.class);
        } catch (Exception e){
            throw new BusinessException(
                    "DATA_INTEGRITY_VIOLATION",
                    "Invalid Order Data");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Service layer:

  • Contains all business rules
  • Throws domain-specific exceptions
  • Never returns ResponseEntity
  • Never handles HTTP concerns

ModelMapper — Clean Entity ↔ DTO Mapping

In production systems, entities should never be exposed directly to APIs.

Entities represent database structure, while APIs should expose stable, client-friendly contracts using DTOs.

This is where ModelMapper is used.

What ModelMapper Does

ModelMapper automatically maps data between objects with similar structures, such as:

  • OrderEntityOrderResponse
  • OrderRequestOrderEntity

It eliminates manual field-by-field mapping code.


How ModelMapper Is Used in This Project

modelMapper.map(orderEntity, OrderResponse.class);
modelMapper.map(orderRequest, OrderEntity.class);
Enter fullscreen mode Exit fullscreen mode

In this implementation:

  • Incoming API requests (OrderRequest) are mapped to persistence entities (OrderEntity)
  • Saved entities are mapped back to response DTOs (OrderResponse)
  • Controllers never deal with entities
  • Services control all mapping logic

ModelMapper — Why We Use It

  • Reduces boilerplate: Removes repetitive manual mapping code and prevents missing fields.
  • Decouples layers: Keeps API DTOs independent from persistence entities.
  • Improves maintainability: Makes refactoring DTOs or entities easier without breaking business logic.
  • Right layer responsibility: Mapping lives in the service layer where business logic and data transformation belong, keeping controllers and repositories clean.

Custom Business Exceptions

public class BusinessException extends RuntimeException {

    private final String errorCode;

    public BusinessException(String message){
        super(message);
        this.errorCode = "BUSINESS_ERROR";
    }

    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() {
        return errorCode;
    }
}
Enter fullscreen mode Exit fullscreen mode
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message){
        super(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

How This Integrates with BaseResponse & GlobalExceptionHandler

Refer Part 2

  • Controllers always return BaseResponse using ResponseBuilder
  • Services throw meaningful domain exceptions
  • @RestControllerAdvice catches exceptions globally
  • One consistent response format across the entire application

Final Result

  • Clean folder structure
  • Clear separation of responsibilities
  • Consistent API responses
  • Centralized error handling
  • Production-ready Spring Boot API design

This concludes the Production-Grade Spring Boot API Design series — based on real project experience and refined personal notes.

The goal was simple:
APIs that are clean, consistent, scalable, and interview-ready.


📌 This blog is based on real project experience and refined personal notes.


Top comments (1)

Collapse
 
shashwathsh profile image
Shashwath S H

Great content, helped a lot to understand exception handling. If you're interested in spring boot related content, follow me for such content. I upload daily on fundamental topics of Spring Boot, you might like that..... Happy learning