Building the Spring Boot Application
Understanding Spring Boot Project Structure
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
commonmerely 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.gradledefines the Gradle project identity.- Our backend remains one Gradle project; capability packages do not require Gradle subprojects.
build.gradledefines 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/javacontains production Java source.src/main/resourcescontains runtime resources and configuration.src/test/javacontains automated test code.- Generated
build/and local.gradle/state are not source-controlled. - The base package is
io.tarkev.ordermanagement. - The main
@SpringBootApplicationclass 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
@ComponentScanconfiguration. - 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.
Serviceremains reserved for external capabilities such asPaymentService.- 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, andservicepackages 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.
