Foundation · Understand
Spring Boot Project Anatomy & Package Design
Understand every source directory, classpath resource, scan boundary, test mirror, build artifact, and feature package in a production-friendly project.
Primary source: Spring Boot 3.5: Structuring Your Code
Lesson 3 of 5
1d streakThe Architectural Question
A folder tree without runtime semantics leaves developers guessing why beans, migrations, templates, static files, and test configuration are not discovered.
What this guide proves
- Root Package & Scanning
- Feature-first Design
- Classpath Resources
The mental model
Root package placement: Placing the `@SpringBootApplication` class in the root package (`com.acme.catalog`) automatically enables `@ComponentScan`, `@EnableAutoConfiguration`, and `@ConfigurationPropertiesScan` across all child packages without requiring explicit base package declarations.
Packaging by Feature vs. Packaging by Layer: Packaging by layer (controllers/, services/, repositories/) creates artificial global buckets where every class must be declared `public`, destroying Java package-private encapsulation. Packaging by feature (`product/`, `order/`) keeps related HTTP, transactional, and persistence components cohesive and enables package-private access.
Multi-Module Boundaries: In enterprise architectures, separating `api` (DTOs, client interfaces), `core` (domain invariants, entities), and `app` (runnable Spring Boot application, web adapters, SQL migrations) enforces unidirectional dependency flows and prevents circular module references at compile time.
Runtime Architecture & Flow
Spring Boot · Project anatomy
Every directory has a runtime or build-time responsibility
Place the application class in a root package, group business code by feature beneath it, mirror packages in tests, and treat classpath resources differently from source code and generated output.
- 01Root package scanning
- 02Feature packages
- 03Package-private encapsulation
- 04Multi-module boundaries
- 05Classpath resources
- 06Test mirror
- 07Generated output
Working Implementation
Runnable snippet path
Predict, run, and change each example
The mental model
Root package placement: Placing the `@SpringBootApplication` class in the root package (`com.acme.catalog`) automatically enables `@ComponentScan`, `@EnableAutoConfiguration`, and `@ConfigurationPropertiesScan` across all child packages without requiring explicit base package declarations.
Packaging by Feature vs. Packaging by Layer: Packaging by layer (controllers/, services/, repositories/) creates artificial global buckets where every class must be declared `public`, destroying Java package-private encapsulation. Packaging by feature (`product/`, `order/`) keeps related HTTP, transactional, and persistence components cohesive and enables package-private access.
Multi-Module Boundaries: In enterprise architectures, separating `api` (DTOs, client interfaces), `core` (domain invariants, entities), and `app` (runnable Spring Boot application, web adapters, SQL migrations) enforces unidirectional dependency flows and prevents circular module references at compile time.
A production-friendly feature-first tree
The root application package becomes the default component-scan boundary. Features keep their HTTP, application, domain, and persistence code close together.
catalog-service/
├── pom.xml
├── mvnw
├── .mvn/wrapper/
└── src/
├── main/
│ ├── java/com/acme/catalog/
│ │ ├── CatalogApplication.java # root scan package
│ │ ├── product/
│ │ │ ├── ProductController.java
│ │ │ ├── ProductService.java
│ │ │ ├── Product.java
│ │ │ └── ProductRepository.java
│ │ └── shared/config/
│ └── resources/
│ ├── application.yml # classpath config
│ ├── db/migration/ # Flyway SQL
│ ├── static/ # directly served assets
│ └── templates/ # server-rendered templates
└── test/
├── java/com/acme/catalog/product/ # mirrors production package
└── resources/application-test.yml
target/ # generated; do not commitRemember
Classes above or beside `com.acme.catalog` are not found by the default component scan.
Change one input, predict the result, then compile or test it before continuing.
Why feature packages age better than global layer buckets
A feature can expose only what neighboring features need. Moving or deleting one capability does not require editing every global layer directory.
// com.acme.catalog.product
// Public application API for the product feature.
package com.acme.catalog.product;
// Keep implementation types package-private when possible:
// ProductController -> HTTP adapter
// ProductService -> transactional use cases
// Product -> domain/persistence model
// ProductRepository -> persistence boundaryRemember
Packages are encapsulation boundaries, not merely folders for class names.
Change one input, predict the result, then compile or test it before continuing.
Load a classpath resource correctly
After packaging, a resource may live inside a JAR and have no ordinary filesystem path. Use the Resource abstraction or class loader.
@Component
class SeedDataLoader {
private final ResourceLoader resources;
SeedDataLoader(ResourceLoader resources) {
this.resources = resources;
}
String readSeedData() throws IOException {
var resource = resources.getResource("classpath:seed/products.json");
try (var input = resource.getInputStream()) {
return new String(input.readAllBytes(), StandardCharsets.UTF_8);
}
}
}Remember
Do not use `new File("src/main/resources/...")`; that source path does not exist inside the packaged JAR.
Change one input, predict the result, then compile or test it before continuing.
Multi-module enterprise packaging layout
Splitting an enterprise service into API, Core, and App modules guarantees architectural separation of concerns and prevents domain logic from leaking into HTTP adapters.
<!-- Root multi-module pom.xml -->
<modules>
<module>catalog-api</module> <!-- DTOs, events, and client interfaces -->
<module>catalog-core</module> <!-- Entities, repository ports, domain logic -->
<module>catalog-app</module> <!-- Spring Boot main class, web adapters, SQL -->
</modules>Remember
catalog-core depends on catalog-api, and catalog-app depends on catalog-core. Enforce unidirectional dependencies to prevent circular module dependencies.
Change one input, predict the result, then compile or test it before continuing.
Synthesis & Production Verification
Deliberate Failure Condition
- Break: Load src/main/resources/seed/catalog.json through Resource.getFile() and run from the executable JAR
- Observe: The IDE run succeeds, but packaged startup fails because the nested JAR entry is not a filesystem File
- Repair: Treat packaged resources as classpath streams and verify discovery from the executable artifact, not only from the IDE.
Production Acceptance Criteria
- Explain main/test Java and resource source sets
- Predict the default component-scan boundary
- Organize by feature and load resources from a packaged JAR
Key Takeaway
Source sets, package roots, and classpath resources have build and runtime semantics; feature-first packaging makes those boundaries visible around change.
Architectural & Technical Review
Core Interview Questions
Q01What is the runtime difference between src/main/resources and an arbitrary project folder?Show answer
The build copies src/main/resources onto the runtime classpath and packages it into the artifact. An arbitrary repository folder is not available unless the build explicitly includes it.
Q02Why should packaged resources normally be read as streams?Show answer
A resource may be a nested JAR entry rather than an operating-system file. Classpath streams work both from exploded development classes and packaged artifacts.
Q03Why prefer top-level feature packages over top-level technical layers?Show answer
Feature packages keep controllers, use cases, domain behavior, and adapters for one capability close together, making change ownership and dependency boundaries easier to see and enforce.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Trusting only the IDE run — IDE classpaths can hide missing packaged resources. Run and inspect the executable JAR before calling the checkpoint complete.
- Pitfall: Treating target or build as source — Generated output is disposable. Any required class or resource must originate in a declared source set.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources