Building the Spring Boot Application

Understanding Spring Boot Project Structure

LuettavaEsikatselu

Katselet ilmaista esikatseluoppituntia.

In the previous lesson, we started:

BACKEND-101
Bootstrap Order Management Backend

and created a minimal Spring Boot application foundation.

Our initial structure looked like:

order-management/
├── build.gradle
├── settings.gradle
├── gradlew
├── gradlew.bat
├── gradle/
│   └── wrapper/
│
└── src/
    ├── main/
    │   ├── java/
    │   │   └── io/
    │   │       └── tarkev/
    │   │           └── ordermanagement/
    │   │               └── OrderManagementApplication.java
    │   │
    │   └── resources/
    │       └── application.yml
    │
    └── test/
        └── java/
            └── io/
                └── tarkev/
                    └── ordermanagement/

This looks simple.

But every part of this structure 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 dozens or hundreds of classes.

If files are placed randomly:

OrderService.java

Utils.java

Controller2.java

Helper.java

Manager.java

DatabaseThing.java

another engineer will struggle to build a mental model of the codebase.

A useful project structure helps answer:

Where does application code live?

Where does configuration live?

Where do tests live?

Where does build configuration live?

Where should a new capability be added?

Which files belong to infrastructure?

Which files represent business concepts?

Structure is not merely cosmetic.

It affects:

discoverability

dependency direction

maintainability

Spring component scanning

testing

code review

Good structure helps engineers understand the system before they read every class.


Repository Root

Start from:

order-management/

This directory represents the complete repository.

As the project evolves, it may contain:

source code

build configuration

documentation

database migrations

container configuration

CI configuration

A new engineer opening the repository should eventually be able to answer:

How do I build this?

How do I run this?

Where is the application code?

Where is the architecture documentation?

The repository root is the entry point to the engineering system.


settings.gradle

Our current file is simple:

rootProject.name = 'order-management'

Its responsibility is:

define the Gradle project identity

For a larger Gradle build, settings.gradle can also include subprojects.

But our application currently uses:

one Gradle project

We do not need to introduce Gradle subprojects.


Gradle Subprojects Are Not Domain Modules

This is an important distinction.

Our architecture contains logical capabilities:

Product

Inventory

Order

Payment

Security

That does not imply:

:product

:inventory

:order

:payment

Gradle subprojects.

These are different ideas.

A Domain or capability boundary is about:

business responsibility

A Gradle module is about:

build structure

For our current backend:

one Gradle project

+

capability-oriented Java packages

is sufficient.

Our modular monolith does not need build-level modules merely to prove that internal boundaries exist.


build.gradle

build.gradle defines how the application is built.

It typically contains:

plugins

Java version

repositories

dependencies

test configuration

build tasks

For example:

plugins {
    id 'java'
    id 'org.springframework.boot' version '<version>'
    id 'io.spring.dependency-management' version '<version>'
}

This tells Gradle which build capabilities our project requires.


Build Configuration Is Not Runtime Configuration

This distinction is essential.

build.gradle answers:

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

For example:

implementation 'org.springframework.boot:spring-boot-starter'

belongs to the build definition.

application.yml answers:

How should the application behave when it runs?

For example:

spring:
  application:
    name: order-management

belongs to runtime configuration.

Conceptually:

build.gradle
→ build-time concerns
application.yml
→ runtime configuration

Do not mix the two responsibilities.


Gradle Wrapper

These files:

gradlew

gradlew.bat

gradle/wrapper/

form the Gradle Wrapper.

Common commands include:

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

The Wrapper allows local development, CI, and fresh machines to use the Gradle tooling defined by the project.

That gives us repeatable builds without relying on each engineer's globally installed Gradle version.


src/main/java

Production Java source belongs under:

src/main/java/

Java packages map naturally to directory structure.

For:

package io.tarkev.ordermanagement;

the file lives under:

src/main/java/
└── io/
    └── tarkev/
        └── ordermanagement/

These directories are not arbitrary folders.

They reflect Java namespaces.


Packages Are Namespaces

Imagine two classes:

io.tarkev.ordermanagement.order.Order

and:

io.tarkev.ordermanagement.somepackage.Order

Java can technically distinguish them because they belong to different packages.

But the fact that Java allows the same class name in multiple packages does not automatically make it a good design.

Packages help us:

group related concepts

avoid naming conflicts

express architecture

control visibility

support framework discovery

A package name should help communicate what part of the system we are looking at.


Base Package

Our application root is:

io.tarkev.ordermanagement

This is the namespace for the Order Management Backend.

As real capabilities arrive, we expect packages such as:

io.tarkev.ordermanagement.product

io.tarkev.ordermanagement.inventory

io.tarkev.ordermanagement.order

io.tarkev.ordermanagement.payment

io.tarkev.ordermanagement.security

These packages represent major responsibility areas.

We still do not create them before implementation needs them.


Why Reverse-Domain Package Naming?

Java applications commonly use namespaces such as:

com.company.application

or:

io.tarkev.ordermanagement

Using an organization-controlled domain-style prefix helps create a distinct namespace.

For our project:

io.tarkev.ordermanagement

is clear and consistent.

This is a naming convention.

It does not require more architectural discussion.


The Application Class

Our entry point is:

package io.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
        );
    }
}

This class remains near the root application package.

One important reason is Spring Boot's default component-scanning behaviour.


Component Scanning

Spring needs to discover Spring-managed components.

Later we may have components such as:

@RestController
class ProductHandler {
}

or application components managed by Spring.

Spring Boot normally begins component scanning from the package containing the:

@SpringBootApplication

class and includes its subpackages.

Our application class lives in:

io.tarkev.ordermanagement

Therefore components under:

io.tarkev.ordermanagement.product

io.tarkev.ordermanagement.inventory

io.tarkev.ordermanagement.order

can naturally fall within the default scan path.


Why Main Class Placement Matters

Suppose the application class were moved to:

io.tarkev.ordermanagement.bootstrap

Now:

io.tarkev.ordermanagement.order

is not a child package of:

io.tarkev.ordermanagement.bootstrap

Default component scanning may therefore not find Order components.

We could fix that by manually configuring scanning.

But we would be solving a problem created by poor package placement.

A cleaner structure avoids the problem in the first place.


Prefer Structure Over Extra Configuration

Spring allows explicit scanning configuration such as:

@ComponentScan(...)

But if the normal package hierarchy already matches our application architecture, we gain little by overriding the default.

A useful principle is:

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

Convention is valuable when it reduces accidental complexity.


@SpringBootApplication Is More Than a Marker

At a high level:

@SpringBootApplication

enables several Spring Boot startup concerns, including:

application configuration

auto-configuration

component discovery

We do not need to memorize its complete internal annotation composition yet.

The important architectural lesson is that the package containing this class influences which application components Spring can discover by default.


src/main/resources

Non-Java runtime resources live under:

src/main/resources/

Our current project contains:

application.yml

Later this directory may also contain things such as:

db/migration/

logging configuration

other runtime resources

when requirements introduce them.


Resources Become Part of the Application Classpath

When Gradle packages the application, resources from:

src/main/resources/

are included in the application artifact.

This is why Spring Boot can discover:

application.yml

and why Flyway will later be able to discover migrations from its conventional location:

db/migration/

when BACKEND-102 introduces Flyway.


application.yml

Our current configuration remains:

spring:
  application:
    name: order-management

Later it may include safe configuration defaults or placeholders needed by the running application.

But it should not become:

a list of secrets

every environment's credentials

unstructured infrastructure configuration

Configuration will evolve deliberately as new requirements appear.


Configuration Is Not Code Organization

Suppose a Payment provider has a configurable base URL.

Bad:

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

hard-coded inside business logic.

The intended direction is:

runtime configuration
        ↓
Payment integration

But we do not need to build that configuration yet.

For now, remember:

src/main/java
→ application behaviour
src/main/resources
→ runtime resources/configuration

src/test/java

Automated test source lives under:

src/test/java/

Its package structure should usually mirror the production code it protects.

Main source:

src/main/java/io/tarkev/ordermanagement/
    OrderManagementApplication.java

Test source:

src/test/java/io/tarkev/ordermanagement/
    OrderManagementApplicationTests.java

This makes navigation predictable.


Tests Should Stay Near Their Concept Semantically

Later:

order/domain/Order.java

can have:

order/domain/OrderTest.java

under the test source tree.

Likewise:

order/application/CreateOrderUseCase.java

can be tested by:

order/application/CreateOrderUseCaseTest.java

Tests remain easy to find because they mirror the capability and responsibility they protect.


Main Code Must Not Depend on Test Code

Dependency direction is:

test
  ↓
main application code

not:

main application code
  ↓
test helpers

Classes under:

src/test/java

are not part of the production application artifact.

Production behaviour must never depend on them.


Test Resources

Later we may also use:

src/test/resources/

for test-specific data such as:

configuration

fixtures

sample files

when tests actually need them.

Again:

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


What About src/main/webapp?

Traditional Java web applications sometimes contain:

src/main/webapp

for WAR-based servlet deployment.

Our project is a Spring Boot backend application.

We do not need that structure.

Avoid copying conventions from a different kind of Java application simply because they are familiar.


Build Output

Gradle generates:

build/

This may contain:

compiled classes

test reports

generated resources

JAR artifacts

For example:

build/libs/

contains packaged application artifacts.

Because build/ is generated:

build/

does not belong in Git.


.gradle/

Local Gradle state may appear under:

.gradle/

This also should generally remain outside source control.

Do not confuse it with:

gradle/wrapper/

which belongs in the repository.

Conceptually:

.gradle/
→ local tool state
gradle/wrapper/
→ versioned project tooling

A Useful Repository Mental Model

Version-controlled repository content includes:

build definition

source code

resources

tests

documentation

Locally generated state includes:

build output

IDE state

tool caches

The first category generally belongs in Git.

The second generally does not.


Capability-Oriented Structure

We previously decided that the backend will be organized primarily around business capabilities.

As implementation grows:

io/tarkev/ordermanagement/
├── OrderManagementApplication.java
│
├── product/
├── inventory/
├── order/
├── payment/
└── security/

The important principle is:

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


Layer-First vs Capability-First

A traditional layer-first project might contain:

controller/

service/

repository/

entity/

dto/

This can work for a very small application.

But imagine a growing system containing:

30 controllers

30 services

many repositories

Opening:

service/

tells you little about which part of the business you are working on.

Capability-first organization keeps related code closer together.


Example of Layer-First Organization

A conventional technical-layer structure might look like:

controller/
├── ProductController.java
├── InventoryController.java
└── OrderController.java

service/
├── ProductService.java
├── InventoryService.java
└── OrderService.java

repository/
├── ProductRepository.java
├── InventoryRepository.java
└── OrderRepository.java

Product-related behaviour is spread across several unrelated top-level directories.

It also encourages generic internal Service classes.

That does not match the vocabulary we established in our architecture.


Our Capability-First Direction

Our project will instead evolve toward something such as:

product/
├── api/
├── application/
├── domain/
└── persistence/

inventory/
├── api/
├── application/
├── domain/
└── persistence/

order/
├── api/
├── application/
├── domain/
└── persistence/

payment/
└── integration/

security/

This is a direction.

It does not mean all of these packages should be created today.


Our Architecture Vocabulary Still Applies

Within this structure:

api
→ Handlers and transport models
application
→ UseCases
domain
→ business state and invariants
persistence
→ database-specific implementation
integration
→ external-provider protocol

And importantly:

Service
→ external/third-party capability

For example:

PaymentService

is meaningful because Payment is external.

We do not organize internal workflows around generic:

ProductService

OrderService

InventoryService

classes.


Start Flat When the Capability Is Small

Suppose Product initially contains only a few classes.

We do not need:

product/api/rest/v1/request/

product/api/rest/v1/response/

product/domain/model/entity/

product/domain/model/value/

product/infrastructure/persistence/jpa/

just because a large architecture template exists.

That can make a five-class capability harder to understand.

A package should exist because it reduces actual cognitive complexity.


But Flat Packages Can Eventually Become Too Large

The opposite problem also exists.

If:

order/

eventually contains 80 classes, a flat directory becomes difficult to navigate.

At that point:

order/
├── api/
├── application/
├── domain/
└── persistence/

becomes useful.

Structure should evolve with the code.

A practical rule is:

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


What Belongs in domain/?

When:

order/domain/

exists, it contains Order business concepts such as:

Order

OrderId

OrderItem

OrderStatus

It does not contain:

OrderHandler

JPA-specific repository implementation

provider HTTP client

because those represent different responsibilities.

The Domain package communicates business meaning.


What Belongs in application/?

Application code coordinates workflows.

For example:

CreateOrderUseCase

CancelOrderUseCase

GetOrderHistoryUseCase

GetOrderDetailUseCase

PayOrderUseCase

UseCases may coordinate:

Domain objects

Repositories

external Services

They should not:

parse HTTP JSON

write provider-specific HTTP code

contain raw SQL

Those belong at other boundaries.


What Belongs in api/?

HTTP-facing code belongs around the API boundary.

For Order, examples may include:

OrderHandler

CreateOrderRequest

OrderResponse

Responsibilities include:

REST contract

transport validation

request mapping

response mapping

Not:

database access

Payment-provider HTTP calls

direct manipulation of persistence state

The API boundary speaks HTTP.


What Belongs in persistence/?

Database-specific concerns belong under persistence.

Examples may include:

Spring Data repositories

JPA mappings

custom PostgreSQL queries

Repository implementations

This is where implementation details such as:

atomic conditional Inventory UPDATE

row-level locking

database projections

will eventually live.

Application UseCases depend on meaningful Repository capabilities.

They do not need to know the persistence mechanism.


What Belongs in integration/?

External protocol code belongs in integration.

For Payment:

payment/
└── integration/

may eventually contain:

PaymentService implementation

ProviderPaymentClient

provider request/response models

provider configuration

The application-facing boundary remains:

PaymentService

Provider-specific types must not leak into:

order/domain/

What Belongs in security/?

Application-wide security integration may eventually contain:

SecurityConfiguration

authenticated identity adaptation

authority configuration

Its responsibility is connecting external authentication and Spring Security to our application.

But business ownership rules still belong to relevant workflows.

For example:

Customer may cancel only their own Order

requires:

CustomerId
+
Order ownership data

and therefore remains part of the Order application workflow.

The security/ package should not become a place for every business authorization rule.


Avoid a utils/ Dumping Ground

A common package is:

utils/

It may eventually collect:

OrderUtils

PriceUtils

InventoryUtils

DateUtils

ValidationUtils

This is often a warning sign.

Utilities can hide responsibilities that actually belong to business concepts.

For example:

OrderUtils.calculateTotal(...)

probably belongs to:

Order

because Order owns its total consistency.

Before creating a utility, ask:

Which capability actually owns this behaviour?


Not Every Utility Is Bad

A genuinely technical, stateless helper may occasionally justify utility-style code.

But generic utility packages should not be the default answer when placement feels difficult.

If ownership is clear, keep the code with that capability.

Only truly generic technical functionality should become shared infrastructure.


Avoid common/ for Domain Concepts

Suppose Product and Order use the same primitive representation for price.

That does not automatically justify:

common/Money.java

Our current project does not even use a Money value object.

It uses:

long cents

The broader principle remains:

Do not move something into common merely because more than one class refers to it.

A shared abstraction should have coherent shared meaning.


Package Visibility Can Protect Boundaries

Java allows package-private visibility when no access modifier is specified.

For example:

class SomeOrderInternalComponent {
}

may remain package-private.

Not every implementation type needs to be:

public

A smaller public surface reduces unnecessary coupling.

A useful mindset is:

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

We will use this selectively as implementation evolves.


Public Types Create Coupling

If every internal class is public, unrelated packages can easily start depending on implementation details.

Later refactoring becomes harder.

Package structure and visibility together can help communicate:

public application boundary

versus:

internal implementation detail

Java packages do not provide perfect module enforcement, but they still give us useful tools.


Spring Components and Package Placement

Later Spring-managed components may use stereotypes such as:

@RestController
@Repository
@Component

Spring also provides:

@Service

as a general stereotype.

But in our project terminology, we do not use Service as the name for ordinary internal workflows.

Those are UseCases.

Service remains reserved conceptually for external capabilities such as:

PaymentService

Package placement under:

io.tarkev.ordermanagement

allows Spring's default component scanning to discover application components naturally.


A Component Outside the Scan Path

Suppose someone creates:

io.tarkev.payment.ProviderPaymentClient

outside:

io.tarkev.ordermanagement

Spring Boot may not automatically discover it through our application's default scan path.

A better placement is:

io.tarkev.ordermanagement.payment.integration

This keeps the integration:

  • inside the application namespace
  • inside the Payment capability
  • inside Spring's natural scan path

Structure and runtime behaviour are connected.


Don't Fix Poor Placement With Overly Broad Scanning

We could add:

@ComponentScan("io.tarkev")

and scan a very broad namespace.

But now unrelated Spring components under that namespace could be discovered accidentally.

Better package organization is usually preferable to compensating with broader framework configuration.


Configuration Classes

Later we may add configuration such as:

PaymentClientConfiguration

SecurityConfiguration

Where should it live?

Prefer keeping capability-specific configuration near the capability.

For example:

payment/integration/PaymentClientConfiguration

and:

security/SecurityConfiguration

rather than one global:

config/

package containing every unrelated configuration class.


Is a config/ Package Always Wrong?

No.

If a configuration concern is genuinely application-wide, a top-level configuration package may be reasonable.

But capability-specific configuration should generally stay close to its owner.

Otherwise:

config/

becomes another technical dumping ground.


Resource Structure Follows Tooling Where Appropriate

When BACKEND-102 introduces Flyway, migrations will live under:

src/main/resources/db/migration/

This is a case where tool convention appropriately influences project structure.

We do not need to reproduce our Java capability package hierarchy under resources merely for symmetry.

Architecture and framework conventions should coexist pragmatically.


Source Structure Is Not Runtime Distribution

Our source may eventually contain:

product/

inventory/

order/

payment/

But at runtime they still become:

one JVM

one Spring Application Context

one application artifact

one deployment

This remains our modular monolith.


A Package Is Not a Microservice

This deserves emphasis.

order/

does not mean:

Order microservice

and:

inventory/

does not mean:

Inventory microservice

They are logical source-code boundaries inside one application.

Deployment boundaries are a separate architectural decision.


A Package Is Also Not Automatically an Aggregate

Likewise:

order/domain/

is a package.

Inside it:

Order
OrderItem
OrderStatus

may participate in an Order Aggregate.

But:

package boundary

and:

Domain consistency boundary

are different concepts.

Do not confuse code organization with Domain modelling.


The Source Tree Should Tell a Story

A future engineer browsing:

src/main/java/io/tarkev/ordermanagement/

should eventually see:

product

inventory

order

payment

security

and immediately recognize the application's main responsibilities.

That is far more informative than a root containing:

impl

manager

helper

common

util

misc

Business vocabulary creates a better map of the system.


Naming Matters

Strong names communicate responsibility.

Examples:

order

inventory

payment

CreateOrderUseCase

OrderRepository

PaymentService

ProviderPaymentClient

Weak generic names include:

Manager

Helper

Processor

Common

Data

Stuff

Generic names are sometimes unavoidable.

But many vague names usually indicate unclear responsibility.


One Primary Class per File

Normal Java convention gives us:

Order.java

containing the primary:

class Order

This improves discoverability and tooling.

Nested types may occasionally make sense.

But we should not place several unrelated classes in one file simply to reduce file count.


Interfaces Do Not Need /impl

Some Java projects use:

service/
service/impl/

with:

OrderService

OrderServiceImpl

even when only one implementation exists.

We do not adopt this pattern automatically.

If an interface represents a meaningful boundary, place it according to its responsibility.

For example:

PaymentService

represents an external capability.

Its provider-specific implementation can remain near:

payment/integration/

No generic:

impl/

directory is required.


Don't Mirror Spring Terminology Everywhere

Spring exposes stereotypes such as:

Controller

Service

Repository

Component

That does not mean our root package structure should be:

controllers/

services/

repositories/

components/

Our application architecture is driven primarily by:

Product

Inventory

Order

Payment

Security

Spring supports that architecture.

Spring does not define our Domain model.


Test Structure Should Follow Production Structure

If production source contains:

order/domain/Order.java

the corresponding test can live under:

order/domain/OrderTest.java

If production source contains:

order/application/CreateOrderUseCase.java

the test can live under:

order/application/CreateOrderUseCaseTest.java

This produces predictable navigation.


Integration Tests Can Communicate Their Boundary Through Naming

Later we may have:

OrderTest

for Domain behaviour.

OrderRepositoryIntegrationTest

for PostgreSQL behaviour.

OrderApiIntegrationTest

for HTTP-level behaviour.

Names should communicate what boundary the test exercises.


Don't Create a Custom Integration-Test Source Set Yet

Gradle can support:

src/integrationTest/java

or other custom source sets.

That can be valuable in a larger project.

But our current test suite is tiny.

Adding custom Gradle source-set configuration now would introduce complexity before it provides value.

Later test volume may justify it.


Documentation Structure

Our design phase already established engineering artifacts such as:

docs/
├── rfcs/
├── adr/
└── incidents/

Examples:

docs/rfcs/001-order-management-architecture.md
docs/adr/001-use-modular-monolith.md
docs/adr/005-use-atomic-inventory-consumption.md

Documentation belongs in the repository because it explains the architecture alongside the code that implements it.


Why Keep Architecture Documents With the Code?

Organizations may also use external documentation platforms.

Repository-local architecture documents provide advantages such as:

versioned with code

reviewable through Pull Requests

easy for engineers to discover

preserved architectural history

A company may use both repository and external documentation.

For this project, repository documentation provides a coherent engineering history.


The Repository Grows Gradually

Current:

order-management/
├── build.gradle
├── settings.gradle
├── gradle/
└── src/

Later:

order-management/
├── build.gradle
├── settings.gradle
├── gradle/
├── src/
├── docs/
├── Dockerfile
├── compose.yaml
└── README.md

But each new artifact should correspond to a real project requirement.


Don't Create Docker Structure Yet

Production containerization is not part of the current implementation work.

Likewise, local dependency infrastructure is introduced when the relevant ticket requires it.

Knowing that:

Dockerfile

compose.yaml

will exist eventually does not mean the first project commit needs placeholders for them.


Don't Create Flyway Directories Before BACKEND-102

The same rule applies to:

src/main/resources/db/migration/

That location becomes meaningful when:

BACKEND-102
Configure PostgreSQL and Flyway

begins.

An empty directory provides no current value.


How Spring Boot Finds application.yml

Spring Boot looks for conventional application configuration resources on the classpath.

Our:

src/main/resources/

content is included in that classpath.

Therefore:

application.yml

can be discovered automatically during startup.

This is an example of:

convention over configuration

in practice.


Convention Over Configuration

Spring Boot provides useful defaults and standard locations around things such as:

application configuration

component scanning

resources

testing integration

These conventions reduce manual configuration.

But:

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

A good engineer understands which conventions the application relies on.


Keep the Application Class Small

OrderManagementApplication should remain bootstrap code.

Bad:

@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 application class is not a business component.

Its responsibility is:

application startup

configuration root

Nothing more.


Where Does Business Logic Go?

Not in:

OrderManagementApplication

Not in:

application.yml

Not in:

build.gradle

Business behaviour belongs inside capability code.

For example:

order/domain/

order/application/

inventory/domain/

depending on the responsibility.

Good structural boundaries reinforce the responsibility boundaries we established in Module 2.


How Do We Decide Where a New Class Goes?

Use two questions.

First:

Which capability owns this?

Then:

What responsibility does this class have?

This creates a repeatable placement strategy.


Example: OrderHandler

Capability:

Order

Responsibility:

HTTP/API boundary

Therefore:

order/api/OrderHandler

Example: CreateOrderUseCase

Capability:

Order

Responsibility:

application workflow

Therefore:

order/application/CreateOrderUseCase

Example: Inventory

Capability:

Inventory

Responsibility:

Domain state and behaviour

Therefore:

inventory/domain/Inventory

Example: ProviderPaymentClient

Capability:

Payment

Responsibility:

external provider integration

Therefore:

payment/integration/ProviderPaymentClient

This method scales much better than relying on vague class categories.


What If You Don't Know Where a Class Belongs?

That uncertainty is often useful design feedback.

Suppose you create:

OrderHelper

but cannot decide where it belongs.

Ask what it actually does.

If it:

calculates Order total

that behaviour likely belongs to:

Order

If it:

maps Order to HTTP response

it belongs near:

order/api/

If it:

loads Orders from PostgreSQL

it belongs near:

order/persistence/

Unclear placement often reveals unclear responsibility.


Structure Can Reveal Bad Design

Suppose we end up with:

shared/BusinessLogicHelper.java

used by Product, Inventory, and Order.

That may indicate the capabilities have become coupled through a generic component.

Project structure is useful not only because it organizes code.

It can make architectural smells visible.


Circular Dependencies

Capability-oriented architecture should avoid cycles such as:

order
  ↓
inventory
  ↓
order

Our architecture already decided that cross-capability workflows are coordinated by UseCases.

Domain objects should not arbitrarily call other capabilities.

Project structure helps make these dependencies easier to notice.


Example: Order Cancellation

Bad direction:

order/domain/Order
    ↓
Inventory application component

Now the Order Domain depends directly on another capability's application layer.

Better:

order/application/CancelOrderUseCase

coordinates:

Order
+
InventoryRepository

while:

Order

remains focused on its own lifecycle rule.


Packages Are Guidelines, Not Magic

Java packages cannot automatically enforce every architecture rule.

An engineer can still import a class from the wrong place.

So architecture also depends on:

team conventions

code review

tests

design documentation

Architecture is both a technical and social system.

Folders alone cannot preserve it.


Avoid Excessively Deep Package Hierarchies

Too deep:

io.tarkev.ordermanagement.order.application.usecase.command.handler.impl

The additional levels provide little useful meaning.

Prefer package levels that represent real boundaries.

For example:

io.tarkev.ordermanagement.order.application

is already expressive.


But Don't Flatten Everything Either

This:

io.tarkev

with every class directly underneath it loses the application's capability structure.

We need enough hierarchy to communicate meaningful boundaries.

No more.

No less.


Packages Should Reflect Stable Concepts

Technical implementation details often change faster than business capabilities.

For example:

Order

Product

Inventory

are relatively stable concepts.

Whereas:

JPA

REST

WebClient

are implementation technologies.

Organizing primarily by capability helps the codebase remain understandable when infrastructure choices evolve.


What About API Versioning in Packages?

Our HTTP API uses:

/api/v1/...

as its public versioned route convention.

That does not require us to create Java packages such as:

order/api/v1/

automatically.

URL versioning and source-package versioning are different decisions.

We add another package level only if it provides useful code organization.


Do We Need an internal/ Package?

Some teams use:

internal/

to signal implementation-only code.

We do not currently need it.

For now, we have:

package-private visibility

capability boundaries

responsibility-oriented packages

which are sufficient.


A Healthy Small Backend Should Look Boring

When Product implementation begins, we may eventually have something like:

io/tarkev/ordermanagement/
├── OrderManagementApplication.java
│
└── product/
    ├── api/
    ├── application/
    ├── domain/
    └── persistence/

Then Inventory arrives:

├── product/
├── inventory/

Then Order:

├── product/
├── inventory/
├── order/

The architecture emerges alongside actual code.

That is healthier than generating the entire final directory tree before implementation begins.


Don't Scaffold the Entire Course

We already know Payment appears later.

That does not justify creating today:

payment/
├── application/
├── domain/
├── integration/
└── persistence/

Our design review explicitly established that we should not create a Payment Domain model before Payment requirements justify one.

The same discipline applies to packages.


Spring Boot's Default Layout Is Only a Starting Point

Spring Boot gives us conventions for:

Java source

resources

configuration

component scanning

testing

But Spring Boot cannot decide:

whether Inventory belongs inside Product

who owns Order cancellation rules

whether historical price belongs to Product or OrderItem

Those are application-design decisions.

The framework provides mechanics.

We provide architecture.


Framework Structure vs Domain Structure

Spring Boot contributes concepts such as:

main application class

configuration

Application Context

component discovery

test support

Our architecture contributes:

Product

Inventory

Order

Payment

Security

responsibility boundaries

Both coexist.

Do not confuse framework conventions with the Domain model.


Project Structure and Runtime Deployment

Because everything currently belongs to one Gradle project and one Spring Boot application:

all capability code
        ↓
one build artifact
        ↓
one container later
        ↓
one deployed application

This matches:

ADR-001
Use a Modular Monolith

We get modular source organization without distributed deployment.


Review the Current Repository

At this stage of BACKEND-101, the repository should still be small:

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

Nothing here pretends that future capabilities already exist.

Nothing here is unnecessary for the current foundation.


Project Structure Review Checklist

When reviewing project structure, ask:

Is the main application class
at the root application package?

Are Spring components beneath
the expected scan path?

Are business capabilities
easy to identify?

Are generated files excluded
from Git?

Is runtime configuration
separate from code?

Are tests placed predictably?

Are we creating empty speculative
package hierarchies?

Is common/ or utils/
becoming a dumping ground?

Are provider or database details
leaking into Domain packages?

Is package nesting still
easy to understand?

These questions are more useful than blindly enforcing one directory template.


Common Mistake: Organizing by File Type Forever

Technical-layer organization often begins with:

controller/

service/

repository/

For a small application, it may appear convenient.

But as the backend grows, this structure hides business boundaries.

Our capability-first direction is therefore not a random stylistic preference.

It follows the architecture we already reviewed and accepted.


Common Mistake: Overusing Service

A codebase may eventually contain:

OrderService

ProductService

InventoryService

ValidationService

NotificationService

But the word Service often fails to explain whether a class:

coordinates a business workflow

contains Domain policy

queries persistence

talks to an external provider

Our project uses more precise terminology:

CreateOrderUseCase

CancelOrderUseCase

ProductRepository

InventoryRepository

PaymentService

The name should communicate the responsibility.


Common Mistake: One Global dto/ Package

A global:

dto/

may eventually contain:

ProductDto

OrderDto

InventoryDto

PaymentDto

Now transport models from unrelated capabilities are grouped together merely because they share a technical category.

Instead:

order/api/OrderResponse

keeps the representation close to the API boundary that owns it.

Transport models should generally stay near their transport boundary.


Common Mistake: entity/ Means Everything

Some codebases use:

entity/

to mean:

JPA persistence entities

But our architecture also uses the Domain term:

Entity

for concepts whose identity matters.

To avoid confusion, we prefer:

order/domain/

for Domain concepts.

If persistence-specific representations later differ from the Domain model, they belong under:

order/persistence/

This makes the responsibility clearer.


Common Mistake: One Global Repository Package

A root-level:

repository/

makes database classes easy to find technically but weakens business ownership.

For example:

InventoryRepository

belongs to the Inventory capability.

That ownership matters when Order creation later coordinates Inventory and Order persistence.


Common Mistake: Hiding Everything Behind Manager

A class called:

OrderManager

could mean almost anything.

Does it:

create Orders?

query Orders?

change state?

perform administration?

call Payment?

The name does not tell us.

Prefer names based on actual responsibility.


Structure Helps Code Review

Suppose a Pull Request adds:

order/domain/ProviderPaymentClient.java

The location itself should trigger a review question:

Why is an external provider client inside the Order Domain?

Clear structure makes responsibility violations easier to notice.

That is valuable.


Structure Helps Onboarding

Imagine a new engineer joins six months later and needs to modify Order cancellation.

With clear structure:

order/

is the obvious starting point.

Within it:

application/CancelOrderUseCase

and:

domain/Order

provide a natural path through the workflow.

Good architecture reduces time spent merely searching for the relevant code.


Structure Helps Refactoring

If the Payment provider changes later:

payment/integration/

is the likely boundary.

If the HTTP representation changes:

order/api/

is the likely boundary.

If Order lifecycle behaviour changes:

order/domain/

is where we expect the core invariant.

Project structure helps make changes more local and predictable.


Structure Is Not Immutable

Suppose Product remains tiny.

We may keep:

product/

relatively flat initially.

Later it grows.

Then reorganizing it into:

product/
├── api/
├── application/
├── domain/
└── persistence/

may improve navigation.

That is normal refactoring.

Initial package layout is not sacred.


But Avoid Constant Restructuring

The opposite extreme is:

reorganize packages every week

because a new article recommends another architecture style.

That creates:

merge conflicts

review noise

history churn

little business value

Refactor structure when the current organization causes a real responsibility or navigation problem.


What to Remember About Spring Boot Structure

Spring Boot gives us useful conventions around:

main application class

component scanning

src/main/resources

application.yml

src/test/java

Gradle integration

Our job is to build a clear business architecture inside those conventions.


BACKEND-101 Progress

After this lesson, we understand the project we created rather than merely having generated it.

We now know:

why the application class
lives at the root package

why package placement
affects component scanning

why build configuration
and runtime configuration differ

why tests commonly mirror
production package structure

why capability-oriented packages
are introduced gradually

why generated artifacts
stay outside Git

BACKEND-101 is not only an executable project anymore.

It is an executable project whose structure we understand.


Engineering Principle

The core principle is:

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

Another:

Use Spring Boot conventions where they help, but let business capabilities shape the application architecture.

And:

Create packages and abstractions when real responsibilities exist—not as placeholders for future code.


Summary

In this lesson, we established that:

  • Repository structure is part of architecture and maintainability.
  • settings.gradle defines the Gradle project identity.
  • Our backend remains one Gradle project; capability packages do not require Gradle subprojects.
  • build.gradle defines build-time concerns such as plugins, dependencies, Java version, tests, and packaging.
  • Runtime configuration belongs separately in resources such as application.yml.
  • Gradle Wrapper files are committed because they support reproducible builds.
  • src/main/java contains production Java source.
  • src/main/resources contains runtime resources and configuration.
  • src/test/java contains automated test code.
  • Generated build/ and local .gradle/ state are not source-controlled.
  • The base package is io.tarkev.ordermanagement.
  • The main @SpringBootApplication class should remain at the root application package.
  • Spring Boot's default component scanning naturally discovers components in subpackages beneath the root application class.
  • Good package placement is preferable to unnecessary broad @ComponentScan configuration.
  • Our application is organized primarily around Product, Inventory, Order, Payment, and Security capabilities.
  • Capability packages do not imply Gradle modules or microservices.
  • The modular monolith still builds into one artifact and runs in one JVM/Application Context.
  • API, application, Domain, persistence, and integration subpackages are introduced when actual complexity justifies them.
  • Internal workflows are represented as UseCases rather than generic internal Services.
  • Service remains reserved for external capabilities such as PaymentService.
  • Domain code should not contain HTTP, persistence, Spring Security, or provider-specific protocol concerns.
  • We should avoid speculative empty package trees.
  • Generic common, utils, manager, and service packages can become dumping grounds when responsibilities are unclear.
  • Package-private visibility can reduce unnecessary coupling.
  • Tests should generally mirror the production concepts they protect.
  • Custom integration-test source sets should not be introduced before test complexity requires them.
  • Repository-local RFCs and ADRs preserve architectural history alongside implementation.