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

35 min

Lesson 3 of 5

1d streak

The 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.

  1. 01Root package scanning
  2. 02Feature packages
  3. 03Package-private encapsulation
  4. 04Multi-module boundaries
  5. 05Classpath resources
  6. 06Test mirror
  7. 07Generated output

Working Implementation

Runnable snippet path

Predict, run, and change each example

4 focused examples

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.

01

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.tree
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 commit

Remember

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.

02

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.

package-info.java
// 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 boundary

Remember

Packages are encapsulation boundaries, not merely folders for class names.

Change one input, predict the result, then compile or test it before continuing.

03

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.

SeedDataLoader.java
@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.

04

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.

pom.xml
<!-- 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

  1. Break: Load src/main/resources/seed/catalog.json through Resource.getFile() and run from the executable JAR
  2. Observe: The IDE run succeeds, but packaged startup fails because the nested JAR entry is not a filesystem File
  3. 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

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