Spring Boot project structure
best practices (2026)
A well-structured Spring Boot project is one you can navigate on day one and still understand six months later. Here’s the package structure, layered architecture, naming conventions, and common mistakes — with real code.
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:
- Presentation layer — REST controllers. Handles HTTP requests and responses. Knows nothing about database queries.
- Service layer — Business logic. Orchestrates calls between repositories. Where validation rules, transactions, and domain behaviour live.
- 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
@RestControllerand@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
@Serviceand@Transactionalwhere 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
@Querywith JPQL or derived method names - Never returns entities directly to controllers
Naming conventions
| Class type | Suffix | Example |
|---|---|---|
| JPA entity | (none) | Product |
| JPA repository | Repository | ProductRepository |
| Service interface | Service | ProductService |
| Service implementation | ServiceImpl | ProductServiceImpl |
| REST controller | Controller | ProductController |
| Data Transfer Object | Dto | ProductDto |
| MapStruct mapper | Mapper | ProductMapper |
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 →