Spring Boot Project Structure Explained
A Spring Boot project looks simple on the surface but has a specific structure with specific conventions. Understanding it upfront saves hours of confusion later.
The Standard Layout
order-service/
├── pom.xml
├── mvnw # Maven Wrapper script (Unix)
├── mvnw.cmd # Maven Wrapper script (Windows)
├── .mvn/
│ └── wrapper/
│ └── maven-wrapper.properties
└── src/
├── main/
│ ├── java/
│ │ └── com/devopsmonk/orderservice/
│ │ └── OrderServiceApplication.java
│ └── resources/
│ ├── application.properties
│ ├── static/
│ └── templates/
└── test/
├── java/
│ └── com/devopsmonk/orderservice/
│ └── OrderServiceApplicationTests.java
└── resources/
This is the Maven Standard Directory Layout — Maven expects code here, and Spring Boot layers its own conventions on top.
pom.xml — Build Configuration
The Maven Project Object Model (POM). It defines:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.0</version>
</parent>
Spring Boot parent — inherits a curated list of dependency versions. You add starters without version numbers; the parent ensures they’re all compatible.
<groupId>com.devopsmonk</groupId>
<artifactId>order-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
Coordinates — your project’s unique identifier in the Maven ecosystem. groupId is typically your organization’s reverse domain name. artifactId is the project name. version uses SNAPSHOT for work-in-progress, a release version (like 1.0.0) for releases.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
Dependencies — every library your project needs. Starters are Spring Boot’s bundled dependency sets. We cover them next.
src/main/java — Application Code
All your Java source code lives here. Spring Boot has one critical convention: the main application class must be in the root package, and all other classes must be in that package or sub-packages.
Why this matters: Component Scanning
@SpringBootApplication triggers a component scan starting from the package of the main class. It finds all @Component, @Service, @Repository, and @Controller classes in that package tree.
com.devopsmonk.orderservice ← @SpringBootApplication here
├── OrderServiceApplication.java
├── controller/
│ └── OrderController.java ← @RestController — found ✓
├── service/
│ └── OrderService.java ← @Service — found ✓
└── repository/
└── OrderRepository.java ← @Repository — found ✓
If you put a class outside the root package, component scanning misses it:
com.devopsmonk ← outside the root package
└── SomeHelper.java ← @Component — NOT found ✗
Recommended package structure
As your application grows, organize by feature (not layer):
com.devopsmonk.orderservice/
├── OrderServiceApplication.java
├── order/
│ ├── OrderController.java
│ ├── OrderService.java
│ ├── OrderRepository.java
│ └── Order.java
├── customer/
│ ├── CustomerController.java
│ ├── CustomerService.java
│ └── Customer.java
└── shared/
└── ApiError.java
Feature-based packaging (orders together, customers together) is easier to navigate than layer-based (all controllers together, all services together). It also makes it easier to extract features into separate services later.
src/main/resources — Configuration and Static Files
application.properties
The primary configuration file. Every Spring Boot property can be set here:
# Server
server.port=8080
server.shutdown=graceful
# Application
spring.application.name=order-service
# Database
spring.datasource.url=jdbc:postgresql://localhost:5432/orders
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD}
# JPA
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
You can use application.yml instead if you prefer YAML:
server:
port: 8080
shutdown: graceful
spring:
application:
name: order-service
datasource:
url: jdbc:postgresql://localhost:5432/orders
YAML is more readable for nested properties. Both formats are equivalent. Pick one and stick with it — don’t mix them in the same project.
Profile-specific configuration
Spring Boot supports multiple configuration files for different environments:
resources/
├── application.properties # base (always loaded)
├── application-dev.properties # loaded when profile=dev
├── application-prod.properties # loaded when profile=prod
└── application-test.properties # loaded when profile=test
Activate a profile:
java -jar app.jar --spring.profiles.active=prod
or as environment variable:
SPRING_PROFILES_ACTIVE=prod java -jar app.jar
Profile-specific properties override the base application.properties. This is how you have different database URLs for dev vs. prod.
static/
Static web files served directly — CSS, JavaScript, images, HTML files that don’t go through a template engine. Access them directly: http://localhost:8080/style.css if you put style.css in static/.
templates/
Server-side HTML templates (Thymeleaf, FreeMarker, Mustache). Only relevant if you’re building a traditional server-rendered web app. For REST APIs (what we’re building), this folder stays empty.
src/test/java — Test Code
Tests mirror the main source structure. Each class in main/java typically has a corresponding test in test/java:
main/java/
└── com/devopsmonk/orderservice/
├── order/
│ ├── OrderController.java
│ └── OrderService.java
test/java/
└── com/devopsmonk/orderservice/
├── order/
│ ├── OrderControllerTest.java
│ └── OrderServiceTest.java
└── OrderServiceApplicationTests.java
OrderServiceApplicationTests.java is generated by Spring Initializr:
@SpringBootTest
class OrderServiceApplicationTests {
@Test
void contextLoads() {
// Verifies the Spring context starts without errors
}
}
This is a smoke test — if the application context fails to start (misconfiguration, missing beans), this test fails. Keep it.
Maven Wrapper (mvnw)
The mvnw script and .mvn/wrapper/ directory are the Maven Wrapper. They download the correct Maven version automatically so everyone on the team builds with the same Maven version — regardless of what’s installed globally.
./mvnw spring-boot:run # run the app
./mvnw test # run tests
./mvnw package # build the JAR
./mvnw clean package # clean build artifacts, then build the JAR
Always use ./mvnw rather than mvn for consistency.
How to Structure a Real Application
As the order-service grows through this series, the structure will look like this:
com.devopsmonk.orderservice/
├── OrderServiceApplication.java
│
├── order/
│ ├── api/
│ │ ├── OrderController.java @RestController
│ │ ├── CreateOrderRequest.java DTO (input)
│ │ └── OrderResponse.java DTO (output)
│ ├── domain/
│ │ ├── Order.java @Entity
│ │ ├── OrderItem.java @Entity
│ │ └── OrderStatus.java enum
│ ├── service/
│ │ └── OrderService.java @Service
│ └── repository/
│ └── OrderRepository.java @Repository / JpaRepository
│
├── customer/
│ ├── api/
│ ├── domain/
│ ├── service/
│ └── repository/
│
└── shared/
├── exception/
│ ├── OrderNotFoundException.java
│ └── GlobalExceptionHandler.java
└── config/
└── SecurityConfig.java
Key rule: never let controllers reference repositories directly. The layer structure should be: Controller → Service → Repository. Services contain business logic; controllers handle HTTP concerns only.
Summary
| Directory | Purpose |
|---|---|
pom.xml | Build config, dependency management |
src/main/java | Application code — main class at root package |
src/main/resources/application.properties | Configuration |
src/main/resources/static/ | Static files (CSS, JS, images) |
src/main/resources/templates/ | Server-side HTML templates |
src/test/java | Test code, mirrors main structure |
mvnw | Maven Wrapper — use this instead of mvn |
The two most important rules:
- Main application class belongs in the root package
- All other classes must be in the root package or a sub-package
Next: How Auto-Configuration Works →
Previous: ← Your First Spring Boot Application