Lesson 20 / 159
লিখিত lesson · 13 মিনিট

Understanding Spring Boot Project Structure

What belongs where in the repository, and how the main class's package decides component scanning.

Preview

আপনি একটি free preview lesson দেখছেন।

In the previous lesson, we completed BACKEND-101 Bootstrap Order Management Backend and created a minimal Spring Boot application foundation. The repository now looks like this:

text
order-management/
├── .gitignore
├── README.md
├── build.gradle
├── settings.gradle
├── gradlew
├── gradlew.bat
├── gradle/
│   └── wrapper/
└── src/
    ├── main/
    │   ├── java/
    │   │   └── com/
    │   │       └── tarkev/
    │   │           └── ordermanagement/
    │   │               └── OrderManagementApplication.java
    │   └── resources/
    │       └── application.yml
    └── test/
        └── java/
            └── com/
                └── tarkev/
                    └── ordermanagement/
                        └── OrderManagementApplicationTests.java

This looks simple, but every part of it has a specific responsibility. The goal of this lesson is not to memorize a directory tree. The goal is:

Understand what belongs where, why it belongs there, and how project structure supports the architecture of a growing backend application.

Project Structure Is Part of Architecture

A backend application may eventually contain hundreds of classes. If files are placed randomly, with OrderService.java, Utils.java, Controller2.java, Helper.java, Manager.java and DatabaseThing.java side by side, another engineer will struggle to build a mental model of the codebase.

A useful structure shows where code, configuration, tests and build files live, where a new capability goes, and which files are infrastructure and which are business concepts. Structure is not merely cosmetic. It affects discoverability, dependency direction, maintainability, Spring component scanning, testing and code review.

The repository root, order-management/, is the entry point to the engineering system. As the project evolves, it will also hold documentation, migrations and container and CI configuration, and a new engineer should learn from it how to build and run the application and where the code and architecture documents are.

One Gradle Project, Not Domain Modules

settings.gradle is a single line, rootProject.name = 'order-management', and defines the Gradle project identity. In a larger build it can also include subprojects, but our application is one Gradle project and does not need them.

This is an important distinction. Our architecture contains the capabilities Product, Inventory, Order, Payment and Security. That does not imply Gradle subprojects such as :product, :inventory and :order. A capability boundary is about business responsibility; a Gradle module is about build structure. One Gradle project plus capability-oriented Java packages is sufficient, and our modular monolith does not need build-level modules merely to prove that internal boundaries exist.

Build Configuration Is Not Runtime Configuration

build.gradle defines how the application is built: plugins, the Java version, repositories, dependencies, test configuration and build tasks. It answers:

What does the project need in order to compile, test and package?

A line such as implementation 'org.springframework.boot:spring-boot-starter' belongs to the build definition. application.yml answers a different question:

How should the application behave when it runs?

Our spring.application.name setting belongs there. Do not mix the two responsibilities.

The wrapper files, gradlew, gradlew.bat and gradle/wrapper/, let local development, CI and fresh machines use the Gradle version the project defines, so builds are repeatable without a globally installed Gradle:

bash
./gradlew build
./gradlew test
./gradlew bootRun

src/main/java and the Base Package

Production Java source belongs under src/main/java/, and packages map to directories: a class in package com.tarkev.ordermanagement; lives under src/main/java/com/tarkev/ordermanagement/. These directories are Java namespaces, not arbitrary folders.

Java can tell com.tarkev.ordermanagement.order.Order and com.tarkev.ordermanagement.somepackage.Order apart, but that does not make two Order classes a good design. Packages group related concepts, avoid naming conflicts, express architecture, control visibility and support framework discovery. A package name should tell us which part of the system we are looking at.

Our application root is com.tarkev.ordermanagement. An organisation-controlled, reverse-domain prefix (the familiar com.company.application pattern) creates a distinct namespace. This is a naming convention and needs no more architectural discussion. As capabilities arrive, we expect com.tarkev.ordermanagement.product, .inventory, .order, .payment and .security, and we still do not create them before implementation needs them.

The Application Class and Component Scanning

Our entry point stays at the root application package:

java
package com.tarkev.ordermanagement;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class OrderManagementApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderManagementApplication.class, args);
    }
}

One important reason is Spring Boot's default component scanning. Spring needs to discover the components it manages, such as a future handler:

java
@RestController
class ProductHandler {
}

Spring Boot scans the package of the @SpringBootApplication class and its subpackages. Because our class lives in com.tarkev.ordermanagement, components under .product, .inventory and .order fall within the default scan path: the package containing this class decides which components Spring discovers by default.

If the class moved to com.tarkev.ordermanagement.bootstrap, com.tarkev.ordermanagement.order would no longer be a child package and default scanning would miss the Order components. Configuring scanning manually would only solve a problem created by poor package placement.

Prefer Structure Over Extra Configuration

Spring allows explicit scanning with @ComponentScan(...), but if the package hierarchy already matches our architecture, overriding the default gains little:

Use Spring Boot conventions when they naturally fit the architecture instead of adding configuration without a reason.

Suppose someone creates com.tarkev.payment.ProviderPaymentClient, outside com.tarkev.ordermanagement. Spring Boot will not discover it through our default scan path. A better placement is com.tarkev.ordermanagement.payment.integration: inside the application namespace, inside the Payment capability and inside Spring's natural scan path. Structure and runtime behaviour are connected. Adding @ComponentScan("com.tarkev") instead would scan a very broad namespace, where unrelated Spring components could be discovered accidentally.

Resources, Configuration and Conventions

Non-Java runtime resources live under src/main/resources/. Today it contains only application.yml; later it may hold migrations, logging configuration and other resources when requirements introduce them. When Gradle packages the application, these resources become part of the classpath. Spring Boot looks for conventional configuration files such as application.yml there, which is why it finds ours without any setup.

Tool conventions shape this directory too. When BACKEND-102 Configure PostgreSQL and Flyway introduces Flyway, migrations will live in its conventional location, src/main/resources/db/migration/. We do not reproduce our Java packages under resources merely for symmetry, and we do not create db/migration/ before BACKEND-102 begins: an empty directory provides no current value.

application.yml may later include safe defaults or placeholders the running application needs, but it should not become a list of secrets, every environment's credentials or unstructured infrastructure configuration. The reverse also holds. A payment provider's base URL hard-coded in business logic is bad:

java
private static final String PAYMENT_URL = "https://provider.example.com";

The intended direction is runtime configuration feeding the Payment integration, which we build when payment work arrives. src/main/java holds application behaviour; src/main/resources holds runtime resources and configuration.

All of this is Spring Boot's convention over configuration: standard locations for configuration, component scanning, resources and testing reduce manual setup. But:

Convention over configuration does not mean we should be unaware of what the framework is doing.

Tests Mirror Production Code

Test source lives under src/test/java/, and its packages mirror the code it protects: OrderManagementApplicationTests sits in the same com/tarkev/ordermanagement/ package as OrderManagementApplication. Later, order/domain/Order.java gets order/domain/OrderTest.java, and order/application/CreateOrderUseCase.java gets order/application/CreateOrderUseCaseTest.java. Names can also communicate the boundary a test exercises: OrderTest for domain behaviour, OrderRepositoryIntegrationTest for PostgreSQL behaviour, OrderApiIntegrationTest for HTTP-level behaviour.

Dependencies run from tests to main code, never the reverse: classes under src/test/java are not in the production artifact, so production behaviour must never depend on them.

src/test/resources/ can later hold test configuration, fixtures and sample files when tests need them. Gradle also supports custom source sets such as src/integrationTest/java, valuable in a larger project, but our suite is tiny and the extra configuration would add complexity before it provides value:

Do not create infrastructure merely because it may be useful someday.

Traditional Java web applications have src/main/webapp for WAR deployment. Ours is an executable JAR, so we do not need it: avoid copying conventions from a different kind of application just because they are familiar.

Build Output and Local State

Gradle generates build/: compiled classes, test reports, generated resources and the packaged JARs in build/libs/. Because it is generated, it does not belong in Git, and neither does local Gradle state in .gradle/. Do not confuse .gradle/, local tool state, with gradle/wrapper/, versioned project tooling.

A useful mental model has two categories. The build definition, source code, resources, tests and documentation are repository content and belong in Git. Build output, IDE state and tool caches are locally generated state and generally do not.

Capability-First Packages

We previously decided to organise the backend around business capabilities, so the root package will grow into product/, inventory/, order/, payment/ and security/ next to OrderManagementApplication.java:

Top-level application packages should make the major business responsibilities visible.

A traditional layer-first project instead uses top-level controller/, service/, repository/, entity/ and dto/ directories. That can work for a very small application, but with 30 controllers and 30 services, opening service/ tells you little about which part of the business you are in, and Product behaviour is spread across unrelated directories. Inside a capability, api, application, domain, persistence and integration subpackages separate responsibilities once there is enough code to separate; lesson 3.07 sets out that convention, and the dumping grounds (utils/, common/, dto/, Manager) to avoid, once we have seen how Spring wires the pieces together.

What matters for the repository today is when packages appear. A five-class Product capability does not need product/api/rest/v1/request/ just because a large architecture template exists, and com.tarkev.ordermanagement.order.application.usecase.command.handler.impl adds levels without adding meaning. The opposite problem also exists: an order/ with 80 classes in one flat directory is hard to navigate.

Create structure to reduce real complexity, not to predict imaginary future complexity.

A healthy small backend therefore looks boring. When Product implementation begins, product/ appears; then inventory/, then order/. We know Payment comes later, but our design review decided not to create a Payment domain model before requirements justify one, and the same discipline applies to its packages. The repository root grows the same way: docs/, a Dockerfile and compose.yaml arrive with the work that needs them. Reorganising a capability that has grown is normal refactoring; reorganising every week because an article recommends another style only creates merge conflicts and review noise.

Package Visibility Protects Boundaries

A type without an access modifier is package-private, so an internal component such as class SomeOrderInternalComponent can stay hidden inside its package. If every internal class is public, unrelated packages start depending on implementation details and refactoring gets harder. Java packages are not perfect module enforcement, but they are useful:

Make a class public because another package genuinely needs it, not simply by default.

Packages Are Not Microservices or Aggregates

However many capability packages the source contains, at runtime they become one JVM, one Spring Application Context, one artifact, later one container and one deployment. That matches ADR-001 Use a Modular Monolith: order/ is a logical source boundary, not an Order microservice. Likewise, order/domain/ is a package. Order, OrderItem and OrderStatus inside it may form an Order aggregate, but a package boundary and a domain consistency boundary are different concepts.

Documentation Lives in the Repository

Our design phase already established docs/rfcs/ (for example 001-order-management-architecture.md), docs/adr/ (001-use-modular-monolith.md, 005-use-atomic-inventory-consumption.md) and docs/incidents/. Even where a company also uses an external wiki, repository documents are versioned with the code, reviewed through pull requests, easy to discover and preserve the architectural history alongside the code that implements it.

Keep the Application Class Small

OrderManagementApplication should remain bootstrap code. This is bad:

java
@SpringBootApplication
public class OrderManagementApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderManagementApplication.class, args);
    }

    public void createOrder() {
        // business logic
    }

    public void adjustInventory() {
        // business logic
    }
}

The main class is responsible for application startup and serving as the configuration root, nothing more. Business behaviour belongs neither there nor in application.yml or build.gradle, but in capability code such as order/domain/ or order/application/.

Practice Exercise 1: Follow the Files Into the JAR

Solutions: lesson 3.09.

In your project, run ./gradlew build. Predict where OrderManagementApplication.java, application.yml and OrderManagementApplicationTests.java end up under build/, and which of them reach the executable JAR. Then check:

bash
find build/classes build/resources -type f
jar tf build/libs/order-management-0.0.1-SNAPSHOT.jar | grep BOOT-INF/classes

build/libs/ holds a second file, order-management-0.0.1-SNAPSHOT-plain.jar. List it the same way and try java -jar on it. What is it missing compared with the executable JAR?

Practice Exercise 2: Who Gets Scanned?

The main class is com.tarkev.ordermanagement.OrderManagementApplication. For each component, decide whether Spring Boot's default scanning finds it:

  1. com.tarkev.ordermanagement.product.api.ProductHandler
  2. com.tarkev.ordermanagement.order.persistence.JpaOrderRepository
  3. com.tarkev.payment.ProviderPaymentClient
  4. com.tarkev.ordermanagementtools.ImportJob
  5. com.tarkev.ordermanagement.InventoryHandler

Then move the main class, on paper, to com.tarkev.ordermanagement.bootstrap. Which of the five does it still find?

Practice Exercise 3: Commit It or Not?

For each path, decide whether it belongs in Git, and say why in one sentence: build/libs/order-management-0.0.1-SNAPSHOT.jar, .gradle/, gradle/wrapper/gradle-wrapper.properties, .idea/workspace.xml, an empty src/main/resources/db/migration/ created "for BACKEND-102", src/main/webapp/ copied from an older project, and docs/adr/001-use-modular-monolith.md.

Knowledge Check

Question 1

Why is moving the main class into bootstrap and adding @ComponentScan("com.tarkev.ordermanagement") a worse fix than keeping the class at the root?

Question 2

Our architecture has Product, Inventory, Order, Payment and Security capabilities. Why are they Java packages and not Gradle subprojects?

Question 3

Why must production code never depend on a class under src/test/java, even though both compile in the same project?

Question 4

A teammate proposes @ComponentScan("com.tarkev") so that "nothing is ever missed". What can go wrong?

Question 5

Why does an internal class stay package-private unless another package needs it?

Knowledge Check Answers

Answer 1

It adds configuration only to repair a placement problem, and the next reader must find that annotation to understand which packages are scanned. At the root, the package hierarchy alone explains the scan path.

Answer 2

A capability boundary is about business responsibility, a Gradle module about build structure. Our modular monolith builds one artifact and runs in one JVM, and packages express the boundaries without the cost of a multi-project build.

Answer 3

Test classes are not packaged into the artifact, so the dependency would compile in the project but fail in production. Dependencies run from tests to main code only.

Answer 4

Spring would discover every component in the organisation's namespace, including ones from other libraries or tools that happen to be on the classpath. The application's composition would then depend on what else is present instead of on its own packages.

Answer 5

Every public class is a promise other packages can depend on. Keeping implementation details package-private limits coupling, so the class can change without breaking unrelated code.

Engineering Principle

Project structure should make system responsibilities easier to discover, not merely sort files by extension or framework stereotype.

Use Spring Boot conventions where they help, but let business capabilities shape the application architecture, and create packages when real responsibilities exist, not as placeholders.

Summary

  • Structure is part of architecture: the build definition (settings.gradle, build.gradle, the wrapper) is separate from runtime configuration (application.yml), and build/ and .gradle/ stay out of Git.
  • src/main/java holds production code, src/main/resources classpath resources, and src/test/java tests that mirror production packages.
  • OrderManagementApplication stays small and at the root package com.tarkev.ordermanagement, so default component scanning finds everything beneath it; good placement beats broad @ComponentScan.
  • The application is organised by capability (Product, Inventory, Order, Payment, Security); responsibility subpackages appear when real complexity justifies them, and 3.07 owns the full convention.
  • Packages are neither Gradle modules, microservices nor aggregates: the modular monolith builds into one artifact and runs in one JVM and Application Context.
  • Package-private visibility reduces unnecessary coupling, and architecture documents live in the repository next to the code.
  • Structure grows with the code: no empty package trees, Docker files, migration folders or test source sets before a requirement needs them.

BACKEND-101 is now an executable project whose structure we understand. In the next lesson, Dependency Injection, we look at how the application's components receive the collaborators they need.

আগে ভাবুন

শীঘ্রই আসছে

Checkpoint প্রশ্ন, যেখানে lesson উত্তর দেখানোর আগে আপনাকে একটি উত্তর বেছে নিতে হবে।