A well-structured Spring Boot project is one you can navigate on day one and still understand six months later. A poorly structured one becomes a source of bugs, merge conflicts, and onboarding friction long before the application grows large.

The layered architecture

The standard Spring Boot project follows a 3-layer architecture:

  1. Presentation layer — REST controllers. Handles HTTP requests and responses. Knows nothing about database queries.
  2. Service layer — Business logic. Orchestrates calls between repositories. Where validation rules, transactions, and domain behaviour live.
  3. Persistence layer — JPA repositories and entities. Talks to the database. Knows nothing about HTTP.

Each layer only talks to the layer directly below it. A controller calls a service. A service calls a repository. A repository never calls a controller. This rule sounds obvious — and it is violated constantly in real codebases, usually in small steps that compound into a mess.

Recommended package structure

There are two common approaches: package by layer and package by feature.

Package by layer (simpler, good for small projects)

com.example.myapp/
├── controller/
│   ├── ProductController.java
│   └── OrderController.java
├── service/
│   ├── ProductService.java
│   ├── ProductServiceImpl.java
│   └── OrderService.java
├── repository/
│   ├── ProductRepository.java
│   └── OrderRepository.java
├── entity/
│   ├── Product.java
│   └── Order.java
├── dto/
│   ├── ProductDto.java
│   └── OrderDto.java
└── mapper/
    ├── ProductMapper.java
    └── OrderMapper.java

Clean and predictable. Works well up to ~10 entities. Navigation becomes harder as the project grows because all controllers are in one folder, all services in another — unrelated concepts sit next to each other.

Package by feature (better for medium and large projects)

com.example.myapp/
├── product/
│   ├── ProductController.java
│   ├── ProductService.java
│   ├── ProductServiceImpl.java
│   ├── ProductRepository.java
│   ├── Product.java
│   ├── ProductDto.java
│   └── ProductMapper.java
├── order/
│   └── ...
└── shared/
    ├── exception/
    │   └── GlobalExceptionHandler.java
    └── config/
        └── JacksonConfig.java

Everything related to a feature lives together. Deleting a feature means deleting one package. The threshold for switching is usually around 8–10 entities.

Want the right structure from line one? SpringBoot Generator produces a correctly structured project for every resource you define.

Generate for free →

What belongs in each layer

Controller layer

  • Annotated with @RestController and @RequestMapping
  • Accepts and returns DTOs only — never entities
  • Delegates all logic to the service layer immediately
  • Handles HTTP status codes explicitly with ResponseEntity
  • Validates request bodies with @Valid
@RestController
@RequestMapping("/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<ProductDto> findById(@PathVariable Long id) {
        return ResponseEntity.ok(productService.findById(id));
    }

    @PostMapping
    public ResponseEntity<ProductDto> create(@Valid @RequestBody ProductDto dto) {
        return ResponseEntity.status(HttpStatus.CREATED)
                             .body(productService.create(dto));
    }
}

Service layer

  • Define an interface (ProductService) and a separate implementation (ProductServiceImpl)
  • Annotated with @Service and @Transactional where appropriate
  • Maps between DTOs and entities using a mapper
  • Contains all business rules and domain logic

Repository layer

  • Extends JpaRepository<Entity, ID>
  • No implementation needed for standard CRUD
  • Custom queries use @Query with JPQL or derived method names
  • Never returns entities directly to controllers

Naming conventions

Class typeSuffixExample
JPA entity(none)Product
JPA repositoryRepositoryProductRepository
Service interfaceServiceProductService
Service implementationServiceImplProductServiceImpl
REST controllerControllerProductController
Data Transfer ObjectDtoProductDto
MapStruct mapperMapperProductMapper

For REST endpoints: use lowercase plural nouns. /products, not /Product or /getProducts. HTTP verbs already express the action — the URL expresses the resource.

Why the DTO pattern matters

The single most common mistake is returning JPA entities directly from controllers. It seems harmless. It causes real problems:

  • Bidirectional relationships cause infinite recursion during JSON serialization
  • Lazy loading exceptions when the JPA session closes before serialization completes
  • You expose your entire database schema — including internal fields
  • Adding a column changes your API response — unintentional breaking change

The fix is a DTO with a MapStruct mapper. Read how to configure them: How to set up Spring Boot with MapStruct and Lombok.

Common mistakes to avoid

1. Business logic in controllers

Controllers should delegate immediately. If you find yourself writing if statements or calling repositories directly in a controller method, move that code to a service.

2. Skipping the service interface

Some developers write only ProductServiceImpl without an interface. This makes unit testing harder and violates the dependency inversion principle. The interface adds 5 lines — it is worth it.

3. One entity = one God service

If OrderService grows to 800 lines, split by use case: OrderCreationService, OrderFulfillmentService. Follow the Single Responsibility Principle.

4. No global exception handler

Without a @ControllerAdvice class, unhandled exceptions bubble up as opaque 500 errors. Define a GlobalExceptionHandler that maps your domain exceptions to meaningful HTTP status codes.

5. Configuration hardcoded in Java classes

Database URLs, credentials, external API keys — these belong in application.properties or environment variables. Use @Value or @ConfigurationProperties to inject them.

Skip the boilerplate entirely

Everything described in this article — the package structure, the layered architecture, the DTO pattern, the naming conventions — is what SpringBoot Generator produces automatically. Define your resources in a guided 4-step form, click Generate, download a ZIP that follows all these best practices from the first line.

Start with the right structure. Generate a production-ready Spring Boot project — free, no account required.

Generate for free →

Continue reading