← Home

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

27 January, 2026


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.

Refer previous parts:

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

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)
        );
    }

Annotations Explained

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;
}

Entities:


DTO Layer — API Contracts

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

@Getter
@Setter
@NoArgsConstructor
public class OrderResponse {
    private Long id;
    private String productName;
}

DTOs:


Repository Layer — Data Access

public interface OrderRepository
        extends JpaRepository<OrderEntity, Long> { }

Repositories:


Service Layer — Business Logic Contract

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

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");
        }
    }
}

Service layer:


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:

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);

In this implementation:


ModelMapper — Why We Use It


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;
    }
}
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message){
        super(message);
    }
}

How This Integrates with BaseResponse & GlobalExceptionHandler

Refer Part 2 : Consistent Responses & Global Exception Handling focuses on clean code structure, controllers, services, DTOs, and dependency injection.

Final Result


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.