Building the Spring Boot Application

Running the Application Locally

ReadingPreview

আপনি একটি 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 build remains the authoritative build verification.
  • ./gradlew bootRun is 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 -d followed 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.