Building the Spring Boot Application
Creating a Spring Boot Application
Until now, we have intentionally avoided writing production code.
We first worked through:
Product Requirement
↓
Acceptance Criteria
↓
Engineering Context
↓
Backlog
↓
Technical Design
↓
RFC
↓
ADR
↓
Design Review
The design is now sufficiently clear.
It is time to begin implementation.
Our first ticket is:
BACKEND-101
Bootstrap Order Management Backend
In this lesson, we will create a minimal but production-oriented Spring Boot application.
The goal is not to create as many folders, dependencies, or abstractions as possible.
The goal is:
Create a clean, reproducible Java application foundation on which the entire Order Management Backend can evolve.
Ticket: BACKEND-101
Our initial backlog defined the ticket as:
# BACKEND-101 — Bootstrap Order Management Backend
## Context
We need the initial Spring Boot application
for backend implementation.
## Scope
- Create the Gradle project.
- Configure Spring Boot.
- Establish the application entry point.
- Add baseline project configuration.
- Verify the application can build and start locally.
## Acceptance Criteria
- The project builds successfully with Gradle.
- The Spring Boot application starts successfully.
- The repository contains the agreed baseline project structure.
- A clean checkout can be built without IDE-specific setup.
## Out of Scope
- Product APIs
- Database schema
- Authentication
- Order logic
- Payment integration
- Production Docker setup
This scope will guide our implementation.
Start With the Ticket, Not Spring Initializr
Creating a Spring Boot project technically takes only a few minutes.
But a backend engineer's first question should not be:
Which checkboxes should I select?
The better question is:
What is the minimum required to complete this ticket correctly?
Right now, we need:
Java project
Gradle build
Spring Boot runtime
application entry point
basic configuration
repeatable build
We do not currently need:
PostgreSQL
Spring Data JPA
Flyway
Spring Security
WebClient
Testcontainers
Payment SDK
Those will arrive when later tickets actually require them.
Avoid Dependency Shopping
A common mistake when creating a Spring Boot project is immediately adding:
Spring Web
Spring Data JPA
PostgreSQL
Spring Security
Redis
Kafka
Actuator
Validation
WebFlux
Lombok
because:
We will probably need them eventually.
That creates:
larger dependency surface
more configuration
more startup behaviour
more things to understand
more accidental architecture
Our principle is:
Add a dependency when a current engineering requirement needs it.
The fact that a dependency may become useful later is not enough reason to install it now.
Initial Technology Choices
Our engineering context already established:
Language
→ Java
Framework
→ Spring Boot
Build Tool
→ Gradle
These are existing project constraints.
We do not need another technology-comparison exercise before implementation.
Our job is to create a good foundation using those choices.
Repository Name
Our project repository is:
order-management
Initially:
order-management/
├── build.gradle
├── settings.gradle
├── gradlew
├── gradlew.bat
├── gradle/
└── src/
Later, the repository may evolve toward:
order-management/
├── src/
├── docs/
│ ├── rfcs/
│ ├── adr/
│ └── incidents/
├── compose.yaml
├── Dockerfile
└── README.md
But we should not create every future file now.
Architecture should grow with real implementation needs.
Why Use the Gradle Wrapper?
A Gradle project should normally be built using:
./gradlew build
instead of requiring every developer to install and manually manage a matching Gradle version.
The Gradle Wrapper includes:
gradlew
gradlew.bat
gradle/wrapper/
These files are committed with the project.
That means:
developer machine
CI environment
another engineer's machine
can all use the project-defined Gradle distribution.
This directly supports our Acceptance Criterion:
A clean checkout can be built without IDE-specific setup.
Build Reproducibility Matters
Weak project setup:
It works from IntelliJ
on my machine.
Stronger project setup:
git clone
./gradlew build
works from a supported Java environment.
An IDE is a useful development tool.
It is not the source of truth for the build.
The repository and Gradle build definition are.
Initial Gradle Settings
A minimal settings.gradle:
rootProject.name = 'order-management'
Its responsibility is simple:
define the Gradle project name
We do not need application architecture configuration here.
Initial build.gradle
A minimal Spring Boot build can look conceptually like this:
plugins {
id 'java'
id 'org.springframework.boot' version '<spring-boot-version>'
id 'io.spring.dependency-management' version '<dependency-management-version>'
}
group = 'io.tarkev'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
The version placeholders are intentional in this lesson.
When the actual project is bootstrapped, the team-supported versions are pinned in the build.
The important thing here is understanding the structure and responsibility of the build.
Why Java Toolchains?
This section:
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
declares the Java baseline expected by the project.
Benefits include:
developer consistency
CI consistency
explicit project requirements
Without an explicit baseline, the build may accidentally depend on whichever Java version happens to be installed locally.
Why Not “Whatever Java Is Installed”?
Imagine:
Engineer A
→ Java 21
Engineer B
→ another Java version
CI
→ another version
Subtle differences may appear in:
compilation
language features
runtime behaviour
tooling
A production project should make its supported Java baseline explicit.
Why Only spring-boot-starter?
At this point, the ticket requires only:
a Spring Boot application
that can start successfully
So:
implementation 'org.springframework.boot:spring-boot-starter'
is sufficient for our current foundation.
We are not exposing REST endpoints yet.
Later:
REST/API work
→ Spring Web
Persistence work
→ Spring Data JPA
→ PostgreSQL driver
→ Flyway
Security work
→ Spring Security
Dependencies should arrive with a reason.
Test Dependency
We include:
testImplementation 'org.springframework.boot:spring-boot-starter-test'
because testing is already part of our Definition of Done.
Testing does not suddenly begin when we reach the dedicated Testing module.
The later module will deepen our testing strategy.
But every engineering change should already have testing appropriate to the behaviour it introduces.
Standard Source Structure
Our project follows the standard Java structure:
src/
├── main/
│ ├── java/
│ └── resources/
└── test/
└── java/
Meaning:
src/main/java
→ application source code
src/main/resources
→ application resources
→ configuration
src/test/java
→ automated tests
Spring Boot works naturally with this structure.
Base Package
Our Tarkev namespace is:
io.tarkev.ordermanagement
So application source lives under:
src/main/java/io/tarkev/ordermanagement/
Java package names provide:
namespace
code organization
component scanning boundary
As the application grows, capabilities may appear below it:
io.tarkev.ordermanagement.product
io.tarkev.ordermanagement.inventory
io.tarkev.ordermanagement.order
io.tarkev.ordermanagement.payment
io.tarkev.ordermanagement.security
We do not need to create those packages yet.
Why the Main Class Belongs at the Root
Our application entry point lives at:
io.tarkev.ordermanagement.OrderManagementApplication
This placement works naturally with Spring Boot's default component scanning.
Conceptually:
io.tarkev.ordermanagement
├── OrderManagementApplication
├── product
├── inventory
└── order
Spring can discover application components in subpackages beneath the root package.
That makes the root a natural home for the main application class.
Application Entry Point
Create:
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 is our application entry point.
What Does main Do?
A normal Java application begins with:
public static void main(String[] args)
Spring Boot does not change that Java rule.
The JVM still starts from main.
Inside it:
SpringApplication.run(...)
bootstraps Spring.
Conceptually:
JVM starts
↓
main(...)
↓
SpringApplication.run(...)
↓
Spring Application Context created
↓
application starts
We will explore the Application Context in more detail later.
@SpringBootApplication
The annotation:
@SpringBootApplication
marks our main Spring Boot application configuration.
For now, think of it as the application bootstrap annotation.
It enables Spring Boot's core startup conventions, including configuration and component discovery behaviour.
We will later examine what this annotation actually combines and how Spring Beans and the Application Context work.
Don't Memorize the Annotation Yet
A beginner may see:
@SpringBootApplication
and think:
I need to memorize this annotation.
A better question is:
What responsibility does it fulfill?
At this stage:
application bootstrap
+
primary configuration entry point
Understanding responsibility is more useful than memorizing annotation names.
Application Configuration
Inside:
src/main/resources/
create:
application.yml
The initial configuration can remain very small:
spring:
application:
name: order-management
That gives the application an explicit name.
We do not add future database, Payment, or security configuration yet.
Why YAML?
Spring Boot supports several configuration formats.
For this project, we use:
application.yml
for structured configuration.
This is a project convention rather than an important architectural decision.
The important thing is consistency.
We do not need an architecture debate about YAML versus .properties.
Never Commit Secrets Into Configuration
Even in the bootstrap phase, establish the correct habit.
Bad:
payment:
api-key: super-secret-key
committed to Git.
Later credentials should come from appropriate external configuration or environment mechanisms.
We will design production configuration more fully in the production-readiness module.
Initial Project Structure
After BACKEND-101, our project can look 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/
That is enough for the initial foundation.
Where Are the Product and Order Packages?
They do not exist yet.
Why?
Because:
BACKEND-101
does not implement Product or Order.
Our architecture says those capabilities will exist.
That does not mean we need empty folders proving that we once drew them in a diagram.
We will create the packages when implementation introduces the corresponding responsibilities.
Avoid Architecture by Empty Folder
A poor bootstrap might create:
product/
handler/
application/
repository/
domain/
dto/
mapper/
inventory/
handler/
application/
repository/
domain/
order/
handler/
application/
repository/
domain/
dto/
mapper/
while every directory remains empty.
That is not meaningful architecture.
It is speculative folder generation.
Good architecture becomes visible as real responsibilities arrive.
First Build
Run:
./gradlew build
Conceptually, Gradle:
reads build.gradle
↓
resolves dependencies
↓
compiles main source
↓
compiles test source
↓
runs tests
↓
packages the application
A successful build ends with something equivalent to:
BUILD SUCCESSFUL
That satisfies an important part of BACKEND-101.
Why Build Before Running?
Starting the application directly from an IDE can hide repository-level build problems.
Running:
./gradlew build
verifies:
project definition
dependency resolution
compilation
tests
packaging
We want the repository build to be healthy before feature work begins.
Running the Application
Run:
./gradlew bootRun
Spring Boot starts the application.
At this stage, we do not necessarily have an HTTP server.
Why?
Because we deliberately have not added Spring Web.
That is fine.
Our Acceptance Criterion says:
Spring Boot application starts successfully.
It does not say:
GET /api/v1/products works.
Product APIs belong to later work.
Running the Built Artifact
After:
./gradlew build
Spring Boot produces an executable application artifact under:
build/libs/
It can also be run outside the IDE:
java -jar build/libs/<application-jar>.jar
This is useful because it demonstrates that the application is genuinely runnable from its build artifact.
The IDE Is Optional to the Build
Developers may use:
IntelliJ IDEA
VS Code
another Java IDE
But the engineering model remains:
Repository
+
Gradle
=
authoritative build
not:
IDE-specific configuration
=
authoritative build
This becomes especially important when CI is introduced.
First Test
A minimal Spring Boot bootstrap test can look like:
package io.tarkev.ordermanagement;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class OrderManagementApplicationTests {
@Test
void contextLoads() {
}
}
The test appears almost empty.
Its purpose is specific:
Can Spring successfully create the application context?
What Does contextLoads() Protect?
Suppose later startup configuration becomes invalid because of:
missing required Bean
broken dependency wiring
invalid Spring configuration
An application-context test can catch some classes of bootstrap failure.
For BACKEND-101, that is reasonable test coverage.
Don't Overvalue contextLoads()
A passing:
contextLoads()
does not prove:
business logic is correct
database behaviour is correct
REST API is correct
authorization is correct
It provides startup-level confidence.
Testing should always correspond to the behaviour being protected.
Testing Starts With the Work
For BACKEND-101, relevant behaviours are:
project compiles
Spring context starts
Gradle build succeeds
So a bootstrap test is appropriate.
Later:
Product behaviour
→ Product tests
Order lifecycle
→ Order tests
PostgreSQL behaviour
→ integration tests
We do not create fake business tests before those behaviours exist.
Build Output
After building:
build/
contains generated output.
That directory should not be committed to Git.
Why?
Because it is reproducible build output.
The repository should primarily contain:
source
build definition
configuration
migrations
documentation
rather than generated artifacts.
.gitignore
A basic .gitignore may contain:
.gradle/
build/
.idea/
*.iml
.vscode/
.DS_Store
Exact IDE rules depend on team conventions.
The important principle is:
Do not commit machine-generated local state unless the repository intentionally owns it.
The Gradle Wrapper Is Different
Do not ignore:
gradlew
gradlew.bat
gradle/wrapper/
These files are intentionally versioned.
They are part of the project's reproducible build tooling.
This is different from generated build output.
Think of it as:
build/
→ generated output
→ ignore
versus:
gradlew + wrapper
→ versioned build tooling
→ commit
README at Bootstrap Stage
A lightweight README.md helps another engineer start the project.
For example:
# Order Management Backend
Backend application for the Commerce Order Management project.
## Requirements
- Supported Java version
## Build
```bash
./gradlew build
```
## Run
```bash
./gradlew bootRun
```
The README should describe what works now.
Do not fill it with fictional future commands.
Clean Checkout Test
One Acceptance Criterion says:
A clean checkout can be built without IDE-specific setup.
Conceptually:
fresh checkout
↓
supported Java available
↓
./gradlew build
should succeed.
This is one of the best tests of project-bootstrap quality.
Hidden Local Dependencies
Imagine the build works only because your machine has:
a manually installed library
IDE-generated source
custom local configuration
uncommitted files
unexplained environment state
Then the project is not reproducible.
BACKEND-101 exists partly to prevent this problem before the codebase becomes larger.
Should We Add PostgreSQL Now?
No.
That belongs to:
BACKEND-102
Configure PostgreSQL and Flyway
Adding PostgreSQL here would:
- expand BACKEND-101 scope
- make first startup depend on database infrastructure
- combine two separate engineering concerns
We deliberately keep tickets bounded.
Should We Add Spring Web Now?
The application will eventually expose REST APIs.
Could we add Spring Web now?
Technically, yes.
But the current ticket does not need an HTTP boundary.
We will introduce Spring Web when the course reaches the work that actually exposes REST APIs.
Until then, it would be an unused future dependency.
Should We Add Actuator Now?
No.
Actuator becomes useful when health checks and metrics become actual production-readiness requirements.
Adding it now without using or understanding it gives us extra behaviour with no current purpose.
We will add it when that requirement arrives.
Should We Add Lombok?
There is no current requirement for it.
Modern Java already gives us:
constructors
records
normal classes
IDE generation
where needed.
Introducing Lombok would add another compile-time dependency and another concept learners need to understand.
This does not mean Lombok is universally bad.
It simply is not justified by BACKEND-101.
Should We Add MapStruct?
No.
We do not even have meaningful DTO-to-Domain mapping yet.
Do not install a solution before the problem exists.
Should We Add Redis or Kafka?
No.
Neither solves a problem in this ticket.
Our architecture explicitly avoids speculative distributed infrastructure.
We will introduce infrastructure only when a requirement justifies it.
Build Configuration Is Production Engineering
Developers sometimes think:
src/main/java
→ real code
build.gradle
→ setup
That distinction is misleading.
A broken build.gradle can block the entire engineering team.
The build defines:
dependencies
Java version
plugins
test execution
packaging
It is part of the engineering system that produces the application.
Treat build changes carefully.
Dependency Versions Must Be Controlled
Framework and dependency versions should not change simply because:
a newer version exists
An upgrade can affect:
runtime behaviour
security posture
compatibility
build behaviour
dependency resolution
In a real team, meaningful dependency upgrades are explicit engineering changes.
Throughout this course, dependencies evolve because the roadmap introduces a need.
Package Naming Matters, but Don't Overthink It
Our agreed base package is:
io.tarkev.ordermanagement
Consistency matters.
We do not need repeated architecture discussions about:
ordermanagement
orderservice
order_management
once the project convention is established.
Spend design energy on consequential decisions.
Application Name
Our configuration includes:
spring:
application:
name: order-management
This is useful because application identity later appears in:
logs
metrics
configuration
deployment
Consistent naming from the beginning reduces unnecessary drift.
0.0.1-SNAPSHOT
The Gradle version:
version = '0.0.1-SNAPSHOT'
is simply an early development artifact version.
We are not designing the final release-versioning strategy in BACKEND-101.
Release/version policy belongs to later delivery work.
What Have We Actually Built?
At this point, almost no business functionality exists.
That is intentional.
We have:
a valid Java project
a Gradle build
the Gradle Wrapper
a Spring Boot runtime
an application entry point
baseline configuration
a bootstrap test
reproducible build/run commands
This is foundation work.
It is not yet feature delivery.
Why This Is Still Meaningful Engineering
Every future backend ticket depends on a healthy application foundation.
If bootstrap is chaotic:
different Java baselines
IDE-only builds
uncontrolled dependencies
random package layout
unreproducible startup
every future feature pays that cost.
BACKEND-101 gives the project a stable starting point.
Definition of Done Review
Now apply our project Definition of Done.
Scope
Did we implement the agreed scope?
[✓] Gradle project exists
[✓] Spring Boot configured
[✓] Application entry point exists
[✓] Baseline configuration exists
[✓] Application builds and starts
Acceptance Criteria
[✓] ./gradlew build succeeds
[✓] Spring Boot application starts
[✓] Baseline project structure exists
[✓] Build does not depend on IDE-specific setup
Testing
Relevant automated test:
[✓] Spring application-context startup test
Existing tests:
[✓] pass
Build
[✓] clean Gradle build succeeds
Scope Discipline
Did we add:
Product API?
Database?
Security?
Payment?
Production Docker setup?
No.
Good.
A feature is not more complete because it includes unrelated future work.
Example Pull Request
A reasonable first Pull Request could be:
## What
Bootstraps the Order Management Backend
as a Spring Boot Gradle application.
## Why
Provides the application foundation required
for subsequent backend capabilities.
## Changes
- Added Gradle project and wrapper
- Added Spring Boot application entry point
- Added baseline application configuration
- Added application-context bootstrap test
- Added basic project documentation
## Testing
- `./gradlew build`
- Application started with `./gradlew bootRun`
## Risks
Low. This introduces the initial application foundation
and no business functionality.
This is a small, focused, reviewable PR.
What Should the Reviewer Check?
Review should focus on the ticket rather than cosmetic details.
Useful questions include:
Does a clean build work?
Is the Java baseline explicit?
Are unnecessary dependencies included?
Is the package root correct?
Can the application start outside the IDE?
Is configuration minimal and safe?
Does the change stay within BACKEND-101 scope?
This keeps review aligned with engineering intent.
Example Review Feedback — Unused PostgreSQL Dependency
Reviewer:
PostgreSQL is included, but this ticket does not configure or use a database. Can we add it in BACKEND-102 instead?
Good feedback.
Response:
Yes.
Remove the unused dependency.
This improves dependency discipline and keeps ticket boundaries clean.
Example Review Feedback — Empty Capability Packages
Reviewer:
There are empty Product, Inventory, Order, and Payment package hierarchies, but nothing uses them.
Also good feedback.
Response:
Remove them.
Create capability structure
when implementation reaches
the corresponding capability.
Our architecture is already captured in:
RFC-001
ADRs
technical design
We do not need empty directories to prove it exists.
Keep the Main Branch Buildable
After BACKEND-101 merges:
./gradlew build
should still succeed on the main branch.
This becomes an ongoing expectation.
Every future change begins from a healthy baseline and should leave one behind.
Small PR, Important Foundation
Our first implementation PR is intentionally small.
That gives us:
easy review
clear scope
simple rollback
clean repository history
Future Git history may tell a coherent story:
BACKEND-101
Bootstrap Order Management Backend
BACKEND-102
Configure PostgreSQL and Flyway
BACKEND-103
Model Product Domain
...
That is easier to understand than one giant initial commit containing half the system.
Spring Boot Is a Tool, Not the Course Goal
Notice how Spring Boot entered this course.
We did not start with:
Today we will learn
@SpringBootApplication.
We started with:
We need to bootstrap
the backend application
we already designed.
Spring Boot solves that engineering need.
This is the mindset we want throughout the course:
Problem first. Technology second.
What We Have Not Learned Yet
We have only touched several Spring concepts:
project structure
dependency injection
Beans
Application Context
configuration
They are upcoming lessons.
We do not need to explain every Spring mechanism before the application can exist.
Learning will follow the evolving system.
Where We Are Now
Module 3 proceeds through:
Creating a Spring Boot Application
↓
Understanding Spring Boot Project Structure
↓
Dependency Injection
↓
Beans and the Application Context
↓
Configuration and Application Properties
↓
Environment-Specific Configuration
↓
Structuring a Backend Codebase
↓
Running the Application Locally
BACKEND-101 will evolve through these lessons.
This lesson establishes the executable foundation.
Engineering Principle
The core principle is:
A project bootstrap should establish the minimum reproducible foundation required for the next piece of meaningful work.
Another:
Dependencies, packages, and infrastructure should be added because the current system needs them—not because we expect every backend might eventually use them.
And:
If the project only works through one developer's IDE, the project is not properly bootstrapped.
Summary
In this lesson, we established that:
- BACKEND-101 is the first implementation ticket.
- The initial application is a Gradle-based Spring Boot project.
- The repository build, not the IDE, is the source of truth.
- Gradle Wrapper makes the project build reproducibly across environments.
- The Java baseline is declared explicitly through a toolchain.
- The Tarkev package root is
io.tarkev.ordermanagement. - The Spring Boot main class provides the application entry point.
@SpringBootApplicationmarks the primary application bootstrap configuration.SpringApplication.run(...)starts the Spring application.- Initial configuration should remain minimal.
spring.application.nameis set toorder-management.- Secrets must not be committed into application configuration.
- We do not pre-install PostgreSQL, JPA, Flyway, Security, Redis, Kafka, Actuator, WebFlux, Lombok, MapStruct, or other dependencies before their requirements arrive.
- Empty package hierarchies do not create meaningful architecture.
./gradlew buildmust succeed from a clean checkout../gradlew bootRunprovides a build-system-driven way to start the application.- The built executable artifact can also run independently from the IDE.
- A basic application-context test is appropriate for the bootstrap ticket.
- Testing begins with the implementation work rather than being postponed.
- Generated
build/output is ignored, while Gradle Wrapper files are committed. - Build configuration is part of production engineering.
- README documentation should describe what actually works now.
- BACKEND-101 remains intentionally separate from BACKEND-102 PostgreSQL/Flyway work.
- The ticket should produce a small, focused, reviewable Pull Request.
- Reviewers should remove unused dependencies and speculative empty structure.
- The main branch should remain buildable after the bootstrap change.
- Spring Boot is being used as a tool for building the system we designed—not as the purpose of the course itself.
Next lesson:
Understanding Spring Boot Project Structure
There, we will examine the project we just created and understand what each part of the repository does, how Java package placement affects Spring Boot behaviour, where configuration, resources, and tests belong, and how to prevent a growing backend from turning into an unstructured collection of files.
