Skip to content

How to Structure a Spring Boot Project: Best Practices

Purpose

I structure my Spring Boot projects using layered architecture and feature-based packages to keep code maintainable as it grows. A poorly organized project becomes a nightmare to navigate, test, and refactor. In this post, I’ll show you the standard structure I use, explain each layer’s responsibilities, and cover when to split your project into multiple modules.

Standard Structure

Spring Boot recommends placing your main application class at the root package. All other packages should be sub-packages underneath it. This lets @SpringBootApplication automatically scan all your components without extra configuration.

Here’s the structure I use:

package com.example.myapp;
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}

And the directory layout:

com.example.myapp
├── MyApplication.java
├── customer/
│ ├── Customer.java
│ ├── CustomerController.java
│ ├── CustomerService.java
│ └── CustomerRepository.java
└── order/
├── Order.java
├── OrderController.java
├── OrderService.java
└── OrderRepository.java

I organize by feature (customer, order) rather than by layer. This groups related code together, making it easier to find and modify.

Each Layer Explained

Controller Layer (Presentation)

Controllers handle HTTP concerns only. They validate input, call services, and return responses. No business logic here.

@RestController
@RequestMapping("/api/customers")
public class CustomerController {
private final CustomerService service;
@GetMapping("/{id}")
ResponseEntity<CustomerResponse> getById(@PathVariable Long id) {
return ResponseEntity.ok(service.findById(id));
}
}

I keep controllers thin. If logic isn’t HTTP-specific, it belongs in the service layer.

Service Layer (Business Logic)

Services contain business rules and orchestrate operations. This is where the real work happens.

@Service
@Transactional
public class CustomerService {
private final CustomerRepository repository;
private final CustomerMapper mapper;
public CustomerResponse findById(Long id) {
Customer customer = repository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Customer not found"));
return mapper.toResponse(customer);
}
}

I use @Transactional at the service level to manage database transactions. Services can call multiple repositories and coordinate complex operations.

Repository Layer (Data Access)

Repositories handle database operations. CRUD and queries only, no business logic.

public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> findByEmail(String email);
@Query("SELECT c FROM Customer c WHERE c.lastPurchaseDate < :date")
List<Customer> findInactiveCustomers(@Param("date") LocalDate date);
}

I keep queries simple. If a query needs complex logic, I consider whether it belongs in the service layer instead.

Entity Layer (Domain)

Entities map to database tables. I keep them focused on data representation.

@Entity
@Table(name = "customers")
public class Customer {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
// Getters, setters, constructors
}

I don’t expose entities directly to APIs. I use DTOs instead to control what data leaves the application.

DTOs, Mappers, and Exceptions

DTOs

I place DTOs in a dto/ package under each feature. For smaller projects, a centralized dto/ package at the root works too.

public record CustomerRequest(
@NotBlank String name,
@Email String email
) {}
public record CustomerResponse(
Long id,
String name,
String email,
LocalDateTime createdAt
) {}

Using Java records makes DTOs concise and immutable. I always use DTOs for API contracts, never entities.

Mappers

I use MapStruct for entity-DTO conversion. It’s type-safe and performs well at compile time.

@Mapper(componentModel = "spring")
public interface CustomerMapper {
Customer toEntity(CustomerRequest request);
CustomerResponse toResponse(Customer customer);
}

For simple cases, I might put mapping methods directly in the service. But anything beyond a few fields gets its own mapper.

Exceptions

I use a global exception handler to centralize error handling. Custom exceptions go in an exception/ package.

@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(404)
.body(new ErrorResponse(ex.getMessage()));
}
}

This keeps controllers clean and ensures consistent error responses across the application.

Configuration Organization

I split configuration classes by domain rather than putting everything in the main application class. This makes testing easier and code more organized.

@Configuration
@EnableMongoAuditing
public class MongoConfiguration {
// MongoDB-specific configuration
}
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
// Security configuration
}

Each configuration handles one concern: security, database, cache, etc. This lets me use @Import in tests to load only what’s needed.

Multi-Module Projects

For larger applications, I split the project into modules. Here’s when I consider it:

  • More than 50-100 domain classes
  • Multiple teams working on different features
  • Need to share code with other projects
  • Build times are becoming too long

A typical multi-module structure:

myapp/
├── myapp-core/ # Domain models, shared utils
├── myapp-customer/ # Customer feature
├── myapp-order/ # Order feature
└── myapp-api/ # Main application with dependencies

The dependency rule is simple: core has no dependencies, feature modules depend on core, and the API module depends on all features. Never create circular dependencies.

Gradle example:

dependencies {
implementation(project(':myapp-core'))
implementation(project(':myapp-customer'))
}

Summary

A well-structured Spring Boot project uses feature-based packages, clear layer separation, and organized configuration. I start with this structure on every new project:

  • Main class at root package
  • Feature-based packages (customer, order)
  • Controller → Service → Repository flow
  • DTOs for API contracts
  • Configuration split by domain
  • Global exception handling

This keeps code maintainable as the project grows and makes it easier for new developers to understand the codebase.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments