Project Structure
Where code, configuration, migrations and tests live in a Spring Boot project, and why the main class must sit in the root package.
src/main/java/com/example/orders/OrdersApplication.java
src/main/java/com/example/orders/order/OrderController.java
src/main/resources/application.properties
src/test/java/com/example/orders/order/OrderControllerTest.javaA Boot project follows the standard Maven layout, which Gradle uses too. Code lives in src/main/java, files that end up on the classpath in src/main/resources, and tests mirror both under src/test. Build output goes to target/ (Maven) or build/ (Gradle), which never belongs in version control.
The root package rule
The main class, annotated with @SpringBootApplication, sits in the top package of your code, for example com.example.orders. Component scanning starts from that package and walks down. A controller in com.example.orders.order is found; one in com.example.shared is silently ignored, and the endpoint returns 404 with no error at start-up. That silence is why this mistake costs so much time.
Never use the default package (no package line). Scanning from it means scanning every jar on the classpath, and Boot warns about it for good reason.
Organise by feature
Put everything for one part of the domain in one package: order holds the controller, service, repository, entity and DTOs for orders. Classes can then be package-private, which keeps other features from reaching into them. Grouping by layer (controllers, services, repositories) forces everything to be public and spreads one change across four folders.
Resources
application.propertiesorapplication.yaml: configuration. Pick one format per project.db/migration: Flyway scripts such asV1__create_orders.sql.static/andtemplates/: web assets and server-side templates. A pure JSON API usually has neither.
compose.yaml, the build file and the wrapper sit at the project root, not in resources.
The test resources trap
A file at src/test/resources/application.properties does not add to the main one; it replaces it, because both have the same classpath name and the test one comes first. Every property you set in main disappears from tests. Use application-test.properties with an active test profile, or @TestPropertySource, to override a few values instead.
Tests should mirror the package of the class they test. That lets them call package-private methods and keeps slice tests such as @WebMvcTest finding the main configuration by searching upwards from their own package.
Example
orders/
pom.xml
mvnw, mvnw.cmd, .mvn/ wrapper: commit these
compose.yaml local PostgreSQL for development
src/
main/
java/com/example/orders/
OrdersApplication.java @SpringBootApplication, the root package
order/
OrderController.java
OrderService.java
OrderRepository.java
Order.java JPA entity
OrderResponse.java record DTO
customer/
...
resources/
application.properties
application-prod.properties
db/migration/V1__create_orders.sql
test/
java/com/example/orders/
OrdersApplicationTests.java
order/OrderControllerTest.java
resources/
application-test.properties
target/ build output: never commit
When to use it
- Diagnose a controller that returns 404 because it sits outside the main class's package.
- Lay out a new service so each domain feature can be read and changed in one folder.
- Override the database URL for tests without losing the rest of the main configuration.
More examples
One feature package, mostly package-private
Only the controller needs to be public, and even that is optional: Spring can create package-private beans. Keeping the service package-private means another feature cannot call it directly and must go through whatever public API you choose to expose, which keeps features from tangling as the code grows.
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
record OrderResponse(long id, String status) {
}
@Service
class OrderService {
List<OrderResponse> openOrders() {
return List.of(new OrderResponse(1, "OPEN"));
}
}
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orders;
OrderController(OrderService orders) {
this.orders = orders;
}
@GetMapping
List<OrderResponse> open() {
return orders.openOrders();
}
}Overriding a few values for tests
Named application-test.properties, this file layers over the main configuration instead of hiding it. Activate it with @ActiveProfiles("test") on a test class. If you name it application.properties instead, the main file vanishes from the test classpath and tests run against defaults you never chose.
# src/test/resources/application-test.properties
# Loaded on top of the main application.properties when the test profile is active.
spring.jpa.show-sql=true
logging.level.com.example.orders=debugA test in the mirrored package
Because the test lives in the same package as OrderService, it can construct the package-private class and call its package-private method directly. A plain unit test like this needs no Spring at all and runs in milliseconds; save the Spring test annotations for when you test wiring or HTTP behaviour.
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.Test;
record OrderResponse(long id, String status) {
}
class OrderService {
List<OrderResponse> openOrders() {
return List.of(new OrderResponse(1, "OPEN"));
}
}
class OrderServiceTest {
@Test
void returnsOnlyOpenOrders() {
var service = new OrderService();
assertThat(service.openOrders())
.extracting(OrderResponse::status)
.containsOnly("OPEN");
}
}
Discussion