Building the Spring Boot Application
Running the Application Locally
আপনি একটি free preview lesson দেখছেন।
আমরা এখন পর্যন্ত Order Management Backend-এর application foundation তৈরি করেছি।
আমাদের কাছে আছে:
Spring Boot application
Gradle build
application configuration
project structure
Dependency Injection foundation
Spring Beans / Application Context understanding
এখন BACKEND-101 — Bootstrap Order Management Backend practically complete করার জন্য আরেকটি গুরুত্বপূর্ণ বিষয় দরকার:
একজন engineer clean checkout থেকে application কীভাবে locally build এবং run করবে?
Local development workflow simple, repeatable, এবং production-oriented হওয়া উচিত।
আমাদের project convention হবে:
Spring Boot Application
↓
runs directly on developer machine
Dependencies
↓
run through Docker Compose
For example:
Order Management Application
↓
localhost
PostgreSQL
↓
Docker Compose
Later Payment বা অন্য local dependency দরকার হলে সেটিও Docker Compose-এর মাধ্যমে চালানো যেতে পারে।
Why Separate the Application From Its Dependencies?
আমরা চাই local development loop fast হোক।
Application code change করার পর যদি প্রতিবার:
build Docker image
↓
restart application container
করতে হয়, development unnecessarily slow হতে পারে।
Instead:
IDE / Gradle
↓
Spring Boot application
directly run করা convenient।
Meanwhile dependencies like PostgreSQL:
Docker Compose
এর মাধ্যমে reproducibly run করা যায়।
This gives us:
fast code iteration
consistent dependency versions
minimal machine setup
Our Local Development Model
Conceptually:
Developer Machine
┌──────────────────────────────┐
│ Spring Boot Application │
│ ./gradlew bootRun │
│ │
│ localhost:<application-port> │
└──────────────┬───────────────┘
│
│ database connection
▼
┌──────────────────────────────┐
│ Docker Compose │
│ │
│ PostgreSQL │
│ localhost:5432 │
└──────────────────────────────┘
The application is not required to run inside Docker for normal local development।
Containerizing the application itself comes later in:
Module 12 — Shipping Software
Why Docker Compose for Dependencies?
Suppose every engineer manually installs PostgreSQL।
Then:
Engineer A
PostgreSQL version X
Engineer B
PostgreSQL version Y
CI
another version
Configuration may differ too:
database name
username
port
extensions
startup options
Now debugging becomes harder।
Docker Compose gives us a versioned dependency definition।
Conceptually:
repository
↓
compose.yaml
↓
same PostgreSQL setup
for the whole team।
What Docker Compose Solves
Docker Compose can define:
which dependency runs
which image/version is used
which ports are exposed
which environment values are provided
which volumes are used
how services are named
Then local startup becomes predictable।
Instead of:
Install PostgreSQL manually and configure it somehow.
we can eventually say:
docker compose up -d
We Are Not Adding PostgreSQL Yet
Important scope distinction।
The current ticket:
BACKEND-101
Bootstrap Order Management Backend
does not yet configure PostgreSQL।
PostgreSQL arrives in:
BACKEND-102
Configure PostgreSQL and Flyway
So this lesson establishes the local-development convention।
When BACKEND-102 starts, Docker Compose will become part of the real repository setup।
Future Local Workflow
Once PostgreSQL is introduced, expected workflow becomes:
1. Start dependencies
docker compose up -d
2. Run application
./gradlew bootRun
Or:
docker compose up -d
java -jar build/libs/<application>.jar
Application and dependencies remain separate processes।
Build Before Run
A healthy local workflow begins with:
./gradlew build
This verifies:
dependency resolution
compilation
tests
packaging
If this fails, fix the build before relying on IDE behaviour।
Running Through Gradle
For normal development:
./gradlew bootRun
Spring Boot starts using the Gradle project configuration।
This is useful because:
no manual JAR packaging required
application runs from current source
Gradle remains the build authority
Running From the IDE
Using IntelliJ IDEA or another IDE is perfectly fine।
The IDE can run:
OrderManagementApplication.main(...)
directly।
But the project must not depend on IDE-specific configuration to work।
A valid repository should still support:
./gradlew build
and:
./gradlew bootRun
from the terminal।
Why Terminal Commands Still Matter
CI does not care that IntelliJ can run the project।
Another engineer may use a different IDE।
Production packaging will not use your local Run button।
So command-line workflow gives us a reproducible baseline।
Running the Packaged JAR
After:
./gradlew build
Spring Boot creates an executable JAR under:
build/libs/
Then:
java -jar build/libs/<application-jar>.jar
runs the packaged application।
This verifies something different from bootRun:
the packaged artifact itself can start
That matters because later deployment will run an application artifact, not source files from an IDE।
Development Run vs Packaged Run
During active coding:
./gradlew bootRun
is convenient।
Before completing a meaningful bootstrap/release change:
./gradlew build
java -jar ...
can verify packaging too।
Both are useful।
Local Dependencies Should Not Be Started by Java Code
Bad:
public static void main(String[] args) {
startPostgres();
SpringApplication.run(...);
}
Application should not attempt to install or launch its infrastructure dependencies।
Infrastructure lifecycle remains outside the application।
For local development:
Docker Compose
owns dependencies।
Spring Boot owns the application process।
Docker Compose Is Development Infrastructure
Later repository may contain:
compose.yaml
with local dependencies।
Conceptually:
services:
postgres:
image: postgres:<version>
ports:
- "5432:5432"
When BACKEND-102 is implemented, we will create the actual configuration properly।
For now the important architectural idea is:
Compose defines local infrastructure, not application business logic.
docker compose up
Typical command:
docker compose up -d
Meaning:
read compose.yaml
create required Docker resources
start dependencies
run them in detached mode
Then:
docker compose ps
can show running services।
Stopping Local Dependencies
Typical:
docker compose down
This stops/removes Compose-managed containers and network resources according to the Compose configuration।
Whether persistent data remains depends on volume configuration।
We will decide that when PostgreSQL setup is implemented।
Don't Delete Local Data Accidentally
Later PostgreSQL will likely use a named volume so restarting containers does not automatically destroy developer data।
There is a difference between:
docker compose down
and commands that also remove volumes।
Developers should understand whether a command:
stops infrastructure
or:
destroys local persisted data
before using it।
Reproducible Dependency Versions
When PostgreSQL is added, avoid:
image: postgres:latest
in a serious project।
Why?
Because latest changes over time।
Two engineers pulling on different days may get different database versions।
Better:
pin an agreed PostgreSQL version
so local dependency behaviour is repeatable।
Exact version belongs to BACKEND-102 implementation।
Compose Is Not Production Architecture
Another important distinction:
Docker Compose for local dependencies
does not imply:
production uses Docker Compose
Production deployment architecture is a separate concern।
Compose here is primarily a local-development tool।
Later production packaging/deployment will be discussed separately।
Application Configuration for Local Dependencies
Once PostgreSQL exists, Spring Boot might connect using something conceptually like:
jdbc:postgresql://localhost:5432/order_management
Why localhost?
Because the Spring Boot application itself runs on the developer machine while PostgreSQL exposes its container port to the host।
Architecture:
Host Application
↓ localhost:5432
Docker PostgreSQL
This Would Be Different If the App Ran in Compose
If both application and PostgreSQL were inside Docker Compose, application would normally use the Compose service hostname rather than localhost।
For example conceptually:
postgres:5432
But that is not our normal local-development model।
Our convention is:
Application → host
Dependencies → Docker Compose
Therefore host-exposed dependency ports matter।
Why This Distinction Matters
A common Docker networking mistake:
Inside container:
localhost
means:
that container itself
not another container or the host।
But since our application is outside Docker during local development:
localhost:5432
can correctly refer to the host-mapped PostgreSQL port।
Understanding where a process runs prevents confusing networking bugs।
Local Configuration
Once database configuration exists, local settings may be provided through:
application-local.yml
environment variables
depending on whether a value is safe to commit।
For example:
database URL
can often have a safe local default।
Credential handling should remain deliberate।
Starting the Application Without Dependencies
At current BACKEND-101 stage, there may be no external dependency required।
So:
./gradlew bootRun
should start directly।
Later, after PostgreSQL becomes required, forgetting:
docker compose up -d
may cause application startup to fail because the database cannot be reached।
That is expected if database connectivity is required at startup।
Failures Should Be Understandable
Suppose PostgreSQL is not running।
Application startup may fail with database connection errors।
Don't immediately change application code।
First check:
Is Docker running?
Is Compose dependency running?
Is the expected port exposed?
Is local configuration correct?
Local debugging should distinguish:
application bug
from:
missing local infrastructure
A Useful Local Debugging Order
When application does not start:
1. Check the Java/Gradle build.
2. Check required dependency containers.
3. Check application configuration.
4. Read the first meaningful startup failure.
5. Fix the actual root cause.
Avoid reacting only to the final stack-trace line।
Spring Boot Startup Logs
When application starts, Spring emits startup logs।
Useful information often includes:
application startup
active profiles
framework initialization
configured infrastructure
startup failures
You don't need to read every log line equally।
Learn to identify:
what component was starting
which dependency failed
what the first relevant exception says
Startup Logs Are Operational Signals
Do not treat startup logs as noise।
If application startup changes from:
2 seconds
to:
30 seconds
after adding infrastructure, that may indicate a problem।
If it repeatedly retries a dependency during startup, logs may reveal it।
Later structured logging makes these signals even more useful।
Don't Print Secrets During Startup
Never log:
database password
API key
authorization token
just to confirm configuration loaded।
You can safely log non-sensitive context such as:
provider hostname
application name
active profile
where appropriate।
Graceful Shutdown
When running:
./gradlew bootRun
you may stop the application using:
Ctrl+C
This sends a termination signal to the process।
Spring Boot then shuts down the Application Context।
Conceptually:
termination requested
↓
stop accepting/processing work appropriately
↓
Spring context closes
↓
managed resources clean up
↓
process exits
Detailed graceful shutdown behaviour comes later in production readiness।
Don't Kill Processes Randomly
During development, use normal process termination when possible।
Hard-killing processes unnecessarily can make debugging resource cleanup difficult।
For dependencies:
docker compose down
is preferable to manually hunting and killing individual containers।
Application and Dependency Lifecycles Are Separate
You may restart Spring Boot many times while keeping PostgreSQL running।
Workflow:
docker compose up -d
↓
PostgreSQL stays running
./gradlew bootRun
↓
edit code
↓
restart app
↓
edit code
↓
restart app
This keeps development fast।
No need to restart PostgreSQL every time Java code changes।
Why Not docker compose down After Every Code Change?
Because PostgreSQL did not change।
Dependency lifecycle should not be coupled unnecessarily to application source lifecycle।
Only restart infrastructure when:
dependency configuration changes
container fails
database reset is intentionally needed
Local Development Loop
A realistic loop:
Start day:
docker compose up -d
Run app:
./gradlew bootRun
Change Java code
Run tests
Restart/re-run application as needed
End work:
stop application
optionally keep or stop dependencies
Simple and predictable।
Tests and Docker Compose
Important distinction:
Local application dependencies can run through Docker Compose, but automated integration tests should not automatically depend on a developer manually starting Compose.
Later database tests will use:
Testcontainers
where appropriate।
Why?
Because CI should be able to run tests without:
someone remembering to start local PostgreSQL first
Compose vs Testcontainers
Use Docker Compose for:
interactive local application development
Use Testcontainers for:
automated integration tests requiring real infrastructure
These solve related but different problems।
Example
Local application:
Spring Boot
↓
Docker Compose PostgreSQL
Repository integration test:
JUnit
↓
Testcontainers PostgreSQL
The test controls its own database lifecycle।
This gives isolation and reproducibility।
Don't Run Unit Tests Against Docker Compose
A domain test like:
OrderTest
does not need Docker।
A UseCase unit test using fake repositories does not need Docker।
Only tests whose purpose includes real infrastructure should pay that cost।
Don't Make ./gradlew test Depend on Manual Compose
Bad:
Developer forgot docker compose up
↓
all tests fail
unless those tests are intentionally configured as local manual tests—which we should avoid as the default।
Automated tests should own the infrastructure they require।
Clean Checkout Experience
Imagine a new engineer clones the repository।
A healthy local workflow should eventually be roughly:
git clone ...
cd order-management
docker compose up -d
./gradlew build
./gradlew bootRun
when database dependencies are present।
Maybe environment variables need configuration too, but those should be documented।
There should not be hidden machine-specific steps।
README Should Be the Entry Point
Eventually README.md should clearly state:
required Java version
Docker requirement
how to start dependencies
how to build
how to run
required local configuration
A new engineer should not need a private chat message to discover the startup sequence।
Example README Direction
Later:
## Local Development
Start dependencies:
```bash
docker compose up -d
Build:
./gradlew build
Run:
./gradlew bootRun
Stop dependencies:
docker compose down
Actual README evolves as dependencies are implemented।
---
# Docker Must Be an Explicit Local Requirement
If Compose is part of local development, README should say that a Docker-compatible runtime is required।
Do not let:
```text
docker compose
appear as an unexplained command。
Developer setup requirements should be visible।
Don't Require Global PostgreSQL
One benefit of Compose:
New engineer does not need:
brew install postgresql
apt install postgresql
manual Windows setup
for this project।
They need Docker and the repository's Compose definition।
This reduces host-machine drift।
Don't Require Global Payment Emulators Either
If later we introduce a local fake provider as a dependency, Compose can define it too।
Conceptually:
services:
postgres:
...
payment-stub:
...
Then:
docker compose up -d
starts the dependency set।
Again, only introduce such services if real development needs them।
Avoid Running Unnecessary Dependencies
If an application only needs PostgreSQL at a stage of development, Compose should not start:
Redis
Kafka
Elasticsearch
RabbitMQ
because "backend projects use them."
Local infrastructure should mirror actual architecture।
Our current design explicitly does not include those systems।
Local Dependency Health
Compose can eventually define dependency health checks where useful।
For example, it may be helpful to know:
PostgreSQL container running
is not always equal to:
PostgreSQL ready to accept connections
Exact health-check configuration belongs with BACKEND-102 local DB setup।
The broader lesson:
Dependency readiness matters, not only container process existence.
docker compose ps
A useful check:
docker compose ps
shows Compose-managed service state।
When debugging local infrastructure, this is often faster than staring at Spring errors first।
docker compose logs
For dependency debugging:
docker compose logs
or for a specific service:
docker compose logs postgres
can show why a dependency failed।
Application logs and dependency logs answer different questions।
Debug at the Right Boundary
Suppose Spring reports:
connection refused
for PostgreSQL।
Check PostgreSQL/container first।
Suppose PostgreSQL is healthy but query fails due to SQL syntax।
Now investigate application/persistence code।
Good engineers identify which boundary owns the failure।
Port Conflicts
Local dependency ports can conflict with services already running on the developer's machine।
For example:
5432 already in use
Possible reason:
globally installed PostgreSQL already running
Compose setup should have documented port expectations।
Avoid random per-developer port changes if possible because they reduce reproducibility।
If overriding is needed, configuration should make it explicit।
Don't Hide Port Problems With Random Numbers
One engineer using:
5432
another:
6432
another:
7432
without a shared convention makes local troubleshooting harder।
Choose sensible defaults and provide a documented override mechanism if necessary।
Persistent Local Data
When PostgreSQL arrives, we need to choose whether database data survives:
docker compose down
Usually yes, through a named volume।
This improves everyday development।
But engineers also need a deliberate way to reset local state when necessary।
We will implement that when database setup is introduced।
Database Reset Should Be Intentional
Don't make normal:
docker compose down
erase all Orders and Products by default if developers expect state to persist।
Likewise don't make it impossible to reset a broken local database।
Local infrastructure should have understandable lifecycle semantics।
Flyway and Local Startup
Once BACKEND-102 introduces Flyway:
PostgreSQL starts
↓
Spring Boot starts
↓
Flyway applies pending migrations
↓
application uses expected schema
That reduces manual database setup।
Developer should not need to copy SQL scripts manually in a particular order।
Local Environment Should Be Disposable
A developer should be able to recover from broken local infrastructure without reinstalling their machine।
Conceptually:
stop/remove local containers
↓
recreate dependencies
↓
application runs again
Docker Compose helps provide this property।
But Application Data May Be Disposable Too
Local data is development data।
Do not rely on local PostgreSQL as the only copy of something important।
Developers should assume local infrastructure can eventually be reset।
Local Dependency Versions Belong in Git
The Compose definition should be versioned।
Then when PostgreSQL version changes:
pull request
↓
review
↓
team gets same update
rather than every engineer upgrading independently।
Infrastructure definition is part of the engineering project।
Compose Changes Need Review
Changing:
PostgreSQL version
port
volume
environment variable
dependency startup options
can affect the whole team।
Treat compose.yaml as real project configuration, not disposable local noise।
Application Process Should Stay Easy to Debug
Running Spring Boot directly on the host makes it easy to:
attach debugger
set breakpoints
inspect stack traces
restart quickly
This is one reason we keep application outside Compose during normal development।
When Might We Run the App in Docker Locally?
Later, when testing:
Docker image
container startup
deployment configuration
we absolutely should run the application container locally।
But that's a different task։
Normal feature development:
host application + Compose dependencies
Container verification:
application Docker image
Both have value at different stages।
Don't Confuse Development Workflow With Delivery Workflow
Local development optimizes for:
fast iteration
debugging
repeatability
Production delivery optimizes for:
immutable artifacts
deployment consistency
operational safety
Later Dockerfile lessons address delivery workflow।
Verifying BACKEND-101 Locally
At the end of bootstrap, before database dependencies exist:
./gradlew clean build
should pass।
Then:
./gradlew bootRun
should start the application।
Then stop it normally।
Also verify the packaged artifact:
./gradlew build
java -jar build/libs/<application-jar>.jar
No IDE-specific step should be required।
Once BACKEND-102 Arrives
The same verification evolves into:
docker compose up -d
./gradlew clean build
./gradlew bootRun
Then:
docker compose down
when appropriate।
The application workflow remains simple even as dependencies grow।
Clean Build
Use:
./gradlew clean build
occasionally to verify the project doesn't depend on stale generated outputs।
clean removes previous build output before rebuilding।
You do not need to run clean before every compile during normal development।
Why Not clean Every Time?
Gradle incremental build features make repeated builds faster।
Normal:
./gradlew build
is enough most of the time।
Use clean build when you intentionally want to verify from fresh build output or troubleshoot stale artifacts।
Check Git Status
Before committing bootstrap/local-development changes:
git status
should not show accidental files such as:
build/
.gradle/
IDE state
local secret files
Good .gitignore matters।
Do Not Commit Local Secrets
If you later use:
.env
or developer-specific local config, ensure sensitive files are ignored appropriately।
Safe examples/templates can be versioned separately।
Application Startup Should Be Boring
A healthy local startup should become predictable:
dependencies available
configuration loaded
ApplicationContext created
application started
If every startup requires manually fixing something different, local development infrastructure needs improvement।
"Boring" is a desirable operational property।
A Practical Local Development Checklist
Before starting work:
[ ] Correct Java version available
[ ] Docker available when dependencies require it
[ ] Required environment configuration present
[ ] docker compose up -d succeeds
[ ] ./gradlew build succeeds
[ ] ./gradlew bootRun starts the application
When troubleshooting:
[ ] Check application logs
[ ] Check docker compose ps
[ ] Check dependency logs
[ ] Check configuration
[ ] Check port conflicts
Common Mistake 1 — Running Everything Manually
Installing and managing PostgreSQL manually per developer creates environment drift।
Use Compose for project dependencies।
Common Mistake 2 — Running the Application in Compose by Default Too Early
This can slow the normal edit/debug loop without current benefit।
Run the Spring Boot application directly during normal development।
Common Mistake 3 — Using latest Dependency Images
Unpinned versions make local behaviour change unexpectedly।
Use agreed versions।
Common Mistake 4 — Tests Depend on Manual Compose
Automated tests should own required test infrastructure, for example with Testcontainers later।
Common Mistake 5 — Committing Secrets to Compose
Bad:
environment:
PAYMENT_API_KEY: actual-production-secret
Compose files in Git must not contain real production secrets।
Common Mistake 6 — Using Production Dependencies Locally
Local development must not accidentally connect to:
production PostgreSQL
production Payment Provider
Use safe local/sandbox dependencies।
Common Mistake 7 — Restarting Every Dependency After Every Code Change
Keep infrastructure running while iterating on Java code։
Restart only what changed।
Common Mistake 8 — Treating Container Running as Dependency Ready
A running container may still be initializing or unhealthy।
Check readiness when relevant।
Common Mistake 9 — Hiding Local Setup in IDE Configuration
Project startup should be possible outside one developer's IDE।
Common Mistake 10 — Adding Infrastructure Not in the Architecture
Don't put Redis, Kafka, or other systems in Compose just because they are easy to add।
Compose should represent dependencies we actually use।
Our Local Development Contract
For this project, the convention is:
Application
↓
runs locally through Gradle / Java / IDE
Dependencies
↓
run through Docker Compose
Automated infrastructure tests:
Testcontainers
where appropriate later।
Application containerization:
Module 12
when shipping/deployment becomes the focus।
This keeps each tool responsible for the problem it solves।
Engineering Principle
The core principle:
Run the Spring Boot application directly for a fast local development loop, and use Docker Compose to provide reproducible local infrastructure dependencies.
Another:
Local development should be easy to reproduce from the repository, not depend on manually configured machine state.
And:
Use Docker Compose for application dependencies; use Testcontainers for automated infrastructure tests; containerize the application itself when delivery becomes the problem being solved.
Summary
In this lesson, we learned that:
- Local application execution and local dependency execution are separate concerns.
- The Spring Boot application should normally run directly on the developer machine.
- Application dependencies such as PostgreSQL should run through Docker Compose.
- This provides a fast Java development loop while keeping dependency setup reproducible.
./gradlew buildremains the authoritative build verification../gradlew bootRunis the standard development run command.- The packaged JAR should also be runnable outside the IDE.
- IDEs are useful but must not become a hidden build/runtime requirement.
- Docker Compose definitions should be committed and reviewed as project infrastructure.
- Dependency images should use agreed versions rather than unbounded
latest. - Local application configuration connects the host-running application to host-exposed dependency ports.
- Docker networking semantics differ if the application itself runs in a container.
- We will not add PostgreSQL Compose configuration until
BACKEND-102. - Once PostgreSQL exists, the normal workflow becomes
docker compose up -dfollowed by./gradlew bootRun. - Dependency lifecycle should remain independent from normal application code restarts.
- Automated tests should not rely on developers manually starting Compose.
- Testcontainers will provide infrastructure for automated integration tests later.
- Local development should use PostgreSQL when persistence behaviour matters rather than replacing it with an in-memory fake.
- Production systems must never be used accidentally by local development or tests.
- Docker Compose is a local-development tool here, not a statement about production deployment architecture.
- Application containerization belongs to the later shipping module.
- Local startup, dependency status, logs, configuration, and port conflicts should be debugged at the correct boundary.
- Local development should become predictable enough that a new engineer can reproduce it from the repository and README.
With this, Module 3 — Building the Spring Boot Application is complete.
Next module:
Module 4 — Domain Modeling
First lesson:
Turning Business Concepts into Domain Models
We will now start turning the concepts from our RFC—Product, Inventory, Order, and OrderItem—into actual Java domain models while preserving the business rules we established before implementation.