Building the Spring Boot Application

Environment-Specific Configuration

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

আগের lesson-এ আমরা application configuration-এর মূল idea শিখেছি।

আমরা দেখেছি:

Code
    ↓
defines application behaviour

আর:

Configuration
    ↓
defines runtime/deployment setup

For example:

database URL

payment provider URL

timeouts

credentials

এসব environment অনুযায়ী change হতে পারে।

এখন real-world question:

Local development, test, non-production, এবং production কি একই configuration ব্যবহার করবে?

Obviously না।

Local developer হয়তো run করবে:

PostgreSQL → localhost

Payment Provider → local fake server

Non-production environment ব্যবহার করতে পারে:

PostgreSQL → non-production database

Payment Provider → sandbox endpoint

Production ব্যবহার করবে:

PostgreSQL → production database

Payment Provider → production endpoint

কিন্তু application code ideally একই থাকবে।

এই lesson-এর goal:

Environment-specific configuration আলাদা করা, কিন্তু application code এবং business behaviour environment-এর নামে branch না করা।


What Is an Environment?

এই context-এ environment হলো application-এর একটি runtime deployment context।

Examples:

local

test

non-production

production

Different organizations different names ব্যবহার করতে পারে:

dev

qa

staging

preprod

prod

Names less important।

Important হলো:

একই application বিভিন্ন runtime context-এ different infrastructure/configuration-এর সঙ্গে run করে।


Our Environment Model

এই course project-এর জন্য simple model:

Local
    ↓
developer machine

Test / CI
    ↓
automated verification

Non-production
    ↓
production-like shared environment

Production
    ↓
real user traffic

আমাদের এখন এর বেশি environment invent করার দরকার নেই।


Same Code, Different Configuration

Ideal model:

Source Code
    ↓
Build
    ↓
Application Artifact

Then:

                ┌── Local Configuration
                │
Application ────┼── Test Configuration
Artifact        │
                ├── Non-prod Configuration
                │
                └── Production Configuration

Application implementation বদলাচ্ছে না।

Only environment-specific runtime settings differ।


Why This Matters

Bad model:

if production:
    use production database

if staging:
    use staging database

if local:
    use localhost

spread throughout Java code।

This creates:

environment knowledge
inside application logic

and makes behaviour harder to reason about।

Better:

Application asks for database configuration.
Deployment environment provides it.

Avoid Environment Logic in UseCases

Bad:

public class PayOrderUseCase {

    public void execute(...) {
        if (isProduction()) {
            // real provider
        } else {
            // fake provider
        }
    }
}

PayOrderUseCase should not know:

production

staging

local

Its responsibility remains:

load Order

verify payment eligibility

call PaymentService

apply confirmed result

Environment-specific provider configuration belongs outside the UseCase।


The Right Boundary

Conceptually:

Environment
    ↓
Configuration
    ↓
PaymentService implementation/configuration
    ↓
PayOrderUseCase

PayOrderUseCase only sees:

PaymentService

not:

"production"
"staging"
"localhost"

Spring Profiles

Spring provides Profiles as one mechanism for activating environment-specific configuration।

Example profile names:

local

test

nonprod

prod

A profile can influence which configuration is active।

For example:

application.yml

application-local.yml

application-test.yml

application-nonprod.yml

application-prod.yml

This can be useful when some non-secret defaults differ by environment।


Base Configuration

Suppose:

spring:
  application:
    name: order-management

payment:
  timeout: 2s

These values may be common across environments।

They belong in:

application.yml

Then environment-specific files only override what actually differs।


Local Configuration

For local development:

# application-local.yml

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/order_management
    username: order_management

payment:
  base-url: http://localhost:9001

This might be reasonable if these values are safe and intended for local use।

Notice:

local database password

still needs deliberate handling।

We should not normalize committing real secrets merely because the profile is local।


Production Configuration

We should generally avoid committing something like:

# application-prod.yml

spring:
  datasource:
    password: actual-production-password

payment:
  api-key: actual-production-api-key

That is unacceptable।

Production secrets should be supplied externally by deployment infrastructure।


Profiles Do Not Replace External Configuration

A common misunderstanding:

If I use application-prod.yml, production configuration problem is solved.

Not necessarily।

Profiles help organize configuration.

But secrets and deployment-owned values should still come from the environment or an appropriate secret-management mechanism।

Think:

Profile
    ↓
selects configuration context

External runtime configuration:

provides sensitive/deployment-specific values

They can work together।


Activating a Profile

Conceptually Spring can activate a profile using a runtime setting such as:

spring.profiles.active=local

The exact way may come from:

environment variable

command-line argument

deployment configuration

For example:

java -jar app.jar \
  --spring.profiles.active=local

Then Spring loads the relevant profile-specific configuration।


Do Not Hardcode the Active Profile

Bad:

spring:
  profiles:
    active: prod

inside shared source configuration।

Now every developer may accidentally start with production-oriented configuration expectations।

The active environment should normally be chosen externally।


Local Should Be Easy, Production Should Be Safe

These goals are not contradictory।

Local development should be convenient:

simple startup

predictable local database

safe fake dependencies

Production should be strict:

no embedded secrets

required configuration

controlled external dependencies

We should not weaken production safety just to make local setup easier।


Local Defaults

Safe local defaults can be valuable।

For example:

payment:
  base-url: http://localhost:9001

under a local profile clearly indicates:

local environment

It does not risk accidentally sending local development traffic to a real provider।

That's a good default।


Dangerous Defaults

Avoid:

If PAYMENT_BASE_URL is missing
→ use production payment provider

This is especially dangerous locally or in tests।

A safer design is usually:

missing required provider URL
→ fail startup

for environments where payment is enabled/required।


Environment Variables

Production deployment might provide:

SPRING_DATASOURCE_URL

SPRING_DATASOURCE_USERNAME

SPRING_DATASOURCE_PASSWORD

PAYMENT_BASE_URL

PAYMENT_API_KEY

Spring configuration can bind these values into application infrastructure।

This enables:

same JAR
+
different deployment configuration

Keep Secrets Outside Git

Never commit:

database passwords

API keys

client secrets

private tokens

into:

application.yml

application-prod.yml

README examples with real values

Even if repository is private।

Private repositories still have:

history

access changes

CI systems

developer clones

Secrets deserve separate handling।


.env Files

Some local development setups use:

.env

files।

They can be convenient for local tooling, but should usually be ignored by Git when they contain developer-specific or secret values।

Example:

.env

might contain:

DB_PASSWORD=local-password

while repository contains:

.env.example

with placeholders:

DB_PASSWORD=

This pattern can improve discoverability without committing secrets।


Don't Assume Spring Automatically Loads Every .env

A .env file is not universally a native Spring Boot configuration mechanism by itself।

It often belongs to:

shell tooling

Docker Compose

IDE run configuration

deployment tooling

The values eventually need to reach Spring through supported configuration sources such as environment variables।

Understand the actual configuration path।


Test Environment

Automated tests require predictable configuration।

For example, persistence integration tests may later use:

Testcontainers PostgreSQL

instead of a developer's local database।

A test should not accidentally depend on:

whatever PostgreSQL happens to be running
on the engineer's laptop

Tests should control their infrastructure where practical।


Unit Tests Usually Need No Environment

If testing:

Order.cancel()

we should not need:

local profile

database URL

payment credentials

Example:

Order order = unpaidOrder();

order.cancel();

assertEquals(
        OrderStatus.CANCELLED,
        order.status()
);

Environment-specific configuration is irrelevant।

That's a sign of good separation।


UseCase Tests Should Also Stay Focused

For:

PayOrderUseCase

we may inject:

FakeOrderRepository

FakePaymentService

directly।

No need to activate:

test profile

just to test application behaviour if Spring infrastructure itself is not under test।


Integration Tests Are Different

If we test:

Spring configuration

database wiring

HTTP handling

security

then environment-specific test configuration becomes relevant।

For example:

test database

test provider stub

test authentication setup

may be part of an integration-test environment।


Test Configuration Must Be Safe

A critical rule:

Automated tests must never accidentally call production systems.

Imagine integration tests using:

PaymentService

and falling back to real production URL because test configuration is missing।

That is unacceptable।

External integration tests should use deliberate sandbox/stub/test endpoints।


Production-Like Does Not Mean Production Credentials

Non-production environments may mimic:

architecture

configuration shape

deployment behaviour

but should still have isolated:

database

credentials

provider sandbox

where appropriate।

Using production secrets in test environments increases blast radius।


Environment-Specific Values

Good candidates:

database hostname

database credentials

third-party base URL

third-party credentials

network timeout

logging verbosity

server port

These can genuinely vary by runtime environment।


What Should Not Usually Vary by Environment?

Core business behaviour should generally remain consistent।

For example:

paid orders cannot be cancelled in v1

should not become:

local → paid orders can be cancelled

prod → paid orders cannot be cancelled

just because profiles make it technically possible।

That would mean we are testing different product behaviour than production।


Beware of Environment-Specific Business Logic

Bad:

if (profile.equals("test")) {
    skipInventoryValidation();
}

Now tests are not exercising real behaviour।

Better:

same business rules
+
controlled test dependencies

Test doubles should replace infrastructure, not silently disable domain correctness।


Profiles Should Not Become Feature Flags

Spring Profiles and product feature flags solve different problems।

Profile:

Which runtime environment/configuration context?

Feature flag:

Should a product capability/behaviour be enabled?

Don't use:

@Profile("new-order-flow")

as a generic replacement for feature-management strategy।

That would blur deployment and product concerns।


Don't Create Profile Explosion

Avoid:

local

local-sakib

local-jalisa

test-fast

test-full

qa

qa2

staging

preprod

prod

prod-eu

prod-special

without real operational reasons।

Too many profiles become hard to understand।

Start with a small environment model।


Our Current Environment Model Is Enough

For this course:

local
test
nonprod
prod

is already sufficient conceptually।

We don't even need to create all profile files immediately।

Files should appear when real configuration differences exist।


Don't Create Empty Profile Files

Bad bootstrap:

application-local.yml
application-test.yml
application-nonprod.yml
application-prod.yml

all empty।

That communicates no useful configuration।

Create environment-specific files only when there is something meaningful to define।


Common Configuration, Small Overrides

Prefer:

application.yml
    ↓
shared defaults

and:

application-local.yml
    ↓
only local differences

rather than duplicating the entire configuration in every environment file।

Duplication creates drift।


Configuration Drift

Suppose four files each contain:

payment:
  timeout: 2s

Then someone updates only three of them।

Now one environment behaves differently unintentionally।

Better:

shared value in application.yml

unless the value genuinely differs।


Explicit Differences Are Easier to Review

If application-prod.yml contains only:

logging:
  level:
    root: INFO

then reviewer can immediately see what production changes।

Large duplicated configuration files hide important differences।


Environment-Specific Bean Selection

Sometimes environment changes the actual implementation rather than just configuration values।

For example, local development might use a fake third-party dependency।

Spring Profiles can technically support:

@Profile("local")
@Component
public class LocalPaymentService
        implements PaymentService {
}

and production:

@Profile("prod")
@Component
public class ProviderPaymentService
        implements PaymentService {
}

But use this carefully।


When Different Implementations Make Sense

A local fake implementation may be useful if:

real provider unavailable locally

real provider interaction costs money

sandbox is unreliable

we need deterministic local development

But don't introduce a fake implementation before we actually reach Payment integration।


Prefer Realistic Integration When Practical

If provider offers a good sandbox, local/non-production may use the real integration code pointed at:

sandbox base URL

rather than a completely different implementation।

Why?

Because then we exercise:

real request construction

real response parsing

real timeout/error handling

earlier।

Different implementation should exist only when useful।


Configuration Difference Is Often Better Than Code Difference

Prefer:

same ProviderPaymentService
+
different base URL

over:

LocalPaymentService
ProdPaymentService

when both environments can use the same protocol।

This keeps environment behaviour closer to production।


Environment-Specific Repository Implementations?

Our production architecture uses PostgreSQL।

We should not make:

local → in-memory repository
prod → PostgreSQL repository

for normal application development unless there is a strong reason।

That can hide:

SQL behaviour

transaction behaviour

constraints

concurrency issues

from local development।

Local development should ideally use PostgreSQL too।


Development Should Resemble Production Where It Matters

Local doesn't need to replicate the entire production platform।

But important persistence semantics should stay similar।

For our application:

PostgreSQL local
PostgreSQL nonprod
PostgreSQL prod

is preferable to:

HashMap local
PostgreSQL prod

for actual persistence development।


Infrastructure Can Differ Without Business Code Knowing

Example:

Local PostgreSQL:
localhost:5432
Production PostgreSQL:
managed internal hostname

Repository code remains the same।

Only connection configuration changes।

That's exactly what environment-specific configuration is for।


Environment and the Handler → UseCase → Repository Flow

Our application flow does not change:

Handler
    ↓
UseCase
    ↓
Repository

Local:

Repository
    ↓
local PostgreSQL

Production:

Repository
    ↓
production PostgreSQL

The UseCase remains identical।


External Service Flow Also Remains Stable

Application:

PayOrderHandler
    ↓
PayOrderUseCase
    ↓
PaymentService

Non-production:

PaymentService
    ↓
provider sandbox

Production:

PaymentService
    ↓
provider production endpoint

Again, business workflow doesn't need environment branching।


Configuration Validation Per Environment

Some values may be required only when a capability is active in that environment।

But avoid making application startup unpredictably permissive।

For production, especially:

missing required database URL

missing payment API key

invalid provider URL

should be detected as early as practical।


Production Should Fail Closed, Not Guess

Suppose production payment API key is absent।

Bad:

use empty string
start anyway

Then every payment fails at runtime।

Better:

configuration invalid
    ↓
startup/deployment fails

This is easier to detect and safer to operate।


Local Development Can Still Have Safe Defaults

For example:

server port

can have a local default।

But:

production credential

should not。

Treat defaults based on risk and ownership।


Configuration Documentation

As environment requirements grow, README should describe local configuration।

Example:

## Local configuration

Required:

- PostgreSQL available locally
- `DB_PASSWORD`
- Payment sandbox configuration when Payment integration is enabled

Do not document actual secrets।

Document:

variable name

purpose

example safe value where appropriate

Example Configuration Table

Later README might contain:

PropertyPurposeLocal Example
spring.datasource.urlPostgreSQL connectionjdbc:postgresql://localhost:5432/order_management
payment.base-urlPayment provider endpointhttp://localhost:9001
payment.timeoutProvider timeout2s

Secret values can be marked:

required externally

without exposing them।


Avoid "Works Only on My Machine"

Environment configuration should be reproducible।

A new engineer should not need hidden knowledge such as:

edit this Java constant

change this secret file manually

run with a mysterious IDE profile

Local setup should be documented and source-controlled where safe।


IDE Run Configuration Is Not the Application Contract

An IDE can make local development convenient।

But if configuration exists only inside one engineer's IntelliJ setup, the project is not reproducible।

Important settings should be expressible through:

documented environment variables

Spring configuration

versioned safe defaults

not personal IDE state alone।


Environment Variables in CI

CI may supply test-specific values such as:

database configuration

test credentials

container endpoints

through the pipeline environment।

Again:

same code
+
CI-controlled configuration

No CI-only source code branch should be required।


Secrets in CI

CI secret stores can inject sensitive values at runtime।

Important:

do not print them

do not echo entire environment

do not store them in build artifacts

A secret being available securely does not mean every process should expose it।


Logging Differences

Environment-specific logging is reasonable।

For example:

Local:

more debugging information

Production:

appropriate operational level

But never rely on logging level as the only protection against sensitive data।

Code should avoid logging secrets at any level।


Environment-Specific Port

Local:

server.port=8080

may be fine।

Production infrastructure may override application port depending on deployment setup।

This is a legitimate runtime configuration difference।

UseCase/domain code should never care what port the application runs on।


Timeouts May Vary Carefully

External Service timeout may vary between environments due to infrastructure differences।

But changing timeout radically can alter observed behaviour।

Configuration changes should still be reviewed।

Example:

2 seconds
→ 30 seconds

can increase:

resource usage

request latency

failure recovery time

Configuration is operational engineering, not harmless metadata।


Environment Variables Should Have Clear Ownership

Don't create random variables such as:

VALUE_1

CONFIG_X

Use meaningful names tied to configuration properties।

This helps:

deployment

debugging

documentation

Example Local Setup

Later, when PostgreSQL exists, local configuration could conceptually be:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/order_management
    username: order_management

with password supplied externally:

SPRING_DATASOURCE_PASSWORD

This gives:

safe versioned connection defaults
+
uncommitted credential

Example Non-Production Setup

Deployment might provide:

SPRING_DATASOURCE_URL
SPRING_DATASOURCE_USERNAME
SPRING_DATASOURCE_PASSWORD

PAYMENT_BASE_URL
PAYMENT_API_KEY

No code changes needed।


Example Production Setup

Production provides its own values through deployment infrastructure।

Application artifact remains the same one that passed testing।

Conceptually:

Commit
    ↓
Build artifact
    ↓
Tests
    ↓
Non-prod
    ↓
Production

with environment-specific configuration supplied at each stage।


Why Not Build a Separate Production JAR?

Suppose:

order-management-staging.jar

order-management-production.jar

contain different hardcoded settings।

Now we cannot be sure the exact code tested in staging is what reached production।

Better:

one immutable artifact
+
external configuration

where practical।


Environment-Specific Configuration Is Not Runtime Mutation

Another distinction:

Environment configuration is typically selected/provided around application startup।

It does not mean operators should arbitrarily mutate every setting while requests are running।

Dynamic configuration is a different architectural problem।

We do not need it in this course project।


Don't Build a Dynamic Configuration Platform

No requirement exists for:

live property refresh

central configuration server

runtime config distribution

Spring/profile/environment configuration is enough for our current backend।

Avoid solving platform problems we do not have।


Secrets Rotation

In real systems, credentials may need rotation।

Externalized secrets make rotation easier than hardcoded source values because application code doesn't need modification।

But exact secret rotation mechanism depends on deployment infrastructure and belongs later in production operations।


Environment-Specific Configuration and Security

Never use environment configuration to bypass authorization rules।

Bad:

local.disable-auth=true

then local application behaves fundamentally differently।

There may be controlled test setups for authentication, but they should not normalize code paths that production never exercises।

Security differences require care।


Prefer Controlled Test Identity

For tests, instead of disabling ownership checks:

provide an authenticated test identity

and exercise the actual authorization behaviour।

This gives higher confidence that production code works।


Environment Parity

We don't need perfect parity between local and production।

Perfect parity may be expensive or impossible।

But maintain parity where it materially affects correctness।

For this project:

same Java runtime baseline

same application code

same PostgreSQL semantics

same business rules

are valuable।

Differences like:

database hostname

credentials

provider sandbox URL

resource size

are expected।


Configuration Layering

A useful model:

application.yml
    ↓
shared safe defaults

then:

profile-specific configuration
    ↓
environment-specific non-secret overrides

then:

deployment/environment variables
    ↓
deployment-owned values and secrets

Exact precedence should follow Spring Boot configuration rules for the version in use।

The principle is more important than memorizing every source today।


Don't Duplicate Secrets Into Profile Files

Even if deployment injects:

PAYMENT_API_KEY

do not copy it into:

application-prod.yml

for convenience।

One source of secret ownership is clearer and safer।


Configuration Review Checklist

When adding environment-specific configuration, ask:

Does this value genuinely differ by environment?

Can it stay in shared configuration instead?

Is it secret?

Should it be supplied externally?

Is the environment difference infrastructure-related
or am I changing business behaviour?

Can the same application artifact still be used?

Will missing configuration fail clearly?

Are we duplicating configuration unnecessarily?

Does local behaviour still resemble production
where correctness matters?

Is this configuration documented?

Common Mistake 1 — Environment Checks in UseCases

Avoid:

if (environment.equals("prod")) {
}

inside business workflows।


Common Mistake 2 — Production Secrets in Git

Avoid:

application-prod.yml

containing real credentials।


Common Mistake 3 — Separate Business Rules Per Environment

Avoid different cancellation or inventory rules in local/test/prod।

Tests should exercise production behaviour।


Common Mistake 4 — Profile Explosion

Too many profiles become hard to understand and maintain।

Use a small environment model।


Common Mistake 5 — Duplicate Entire Configuration Files

Override only what differs।

Keep common configuration shared।


Common Mistake 6 — Fake Local Persistence

Using a HashMap locally while production uses PostgreSQL can hide real persistence behaviour।

Use PostgreSQL locally when persistence matters।


Common Mistake 7 — Production Fallback

Never make missing configuration silently fall back to production endpoints or unsafe defaults।


Common Mistake 8 — Environment Configuration Only in IDE

Local startup should not depend on one engineer's private IDE settings।

Document the configuration contract।


Common Mistake 9 — Using Profiles as Feature Flags

Environment selection and product feature management are separate concerns।


Common Mistake 10 — Disabling Real Rules in Tests

Don't skip inventory/security/business validation simply because the environment is test

Use controlled dependencies instead।


How This Fits Our Architecture

Application code remains:

Handler
    ↓
UseCase
    ↓
Repository

and:

UseCase
    ↓
External Service

Environment-specific configuration changes:

where Repository connects

where External Service connects

which credentials/configuration infrastructure uses

It does not change the responsibility flow।


Example: Local

CreateOrderHandler
        ↓
CreateOrderUseCase
        ↓
OrderRepository
        ↓
Local PostgreSQL

Example: Production

CreateOrderHandler
        ↓
CreateOrderUseCase
        ↓
OrderRepository
        ↓
Production PostgreSQL

Same Handler।

Same UseCase।

Same repository behaviour।

Different runtime connection configuration।


Example: Payment

Non-production:

PayOrderUseCase
        ↓
PaymentService
        ↓
Provider Sandbox

Production:

PayOrderUseCase
        ↓
PaymentService
        ↓
Production Provider

Again, environment details remain at the infrastructure boundary।


What Should Exist in Our Project Right Now?

At this stage, our application may still only need:

spring:
  application:
    name: order-management

We do not need to create all environment profile files yet।

This lesson establishes the design rules before PostgreSQL and other runtime dependencies arrive।


When Will We Apply This?

BACKEND-102 will introduce:

PostgreSQL
Flyway

Then local/test database configuration becomes real।

Later Payment integration will introduce:

provider endpoint

credentials

timeouts

Then non-production vs production external configuration becomes real।

We add environment-specific configuration when the capability actually requires it।


Engineering Principle

The core principle:

Environment changes should change runtime configuration, not the application's business architecture or source code.

Another:

Build once, then provide environment-specific infrastructure settings externally wherever practical.

And:

Keep business behaviour consistent across environments; replace or reconfigure infrastructure at the boundaries.


Summary

In this lesson, we learned that:

  • An environment is a runtime deployment context such as local, test, non-production, or production.
  • The same application code should ideally run across environments.
  • The same built artifact should be reusable with different runtime configuration.
  • Environment-specific logic should not live inside Handlers, UseCases, or domain objects.
  • Spring Profiles can help organize environment-specific configuration.
  • application.yml should hold shared safe defaults where appropriate.
  • Profile-specific files should override only meaningful differences.
  • Profiles do not replace proper external secret handling.
  • Active profiles should normally be selected externally rather than hardcoded.
  • Production credentials must not be committed to Git.
  • Environment variables can provide deployment-owned configuration.
  • .env files may help local tooling but should not be assumed to be automatically loaded by Spring.
  • Automated tests should use controlled test infrastructure and must never accidentally call production systems.
  • Core business rules should remain consistent between test and production.
  • Profiles should not be misused as feature flags.
  • Avoid profile explosion and duplicated configuration files.
  • Local persistence should use PostgreSQL when real persistence behaviour matters.
  • Configuration differences should preferably change endpoints/credentials rather than swap entire implementations when the same integration code can be reused.
  • Missing required production configuration should fail clearly rather than use dangerous fallbacks.
  • IDE-only configuration is not a reproducible project setup.
  • Environment-specific configuration is an infrastructure concern around our Handler → UseCase → Repository architecture, not part of the business workflow itself.

Next lesson:

Structuring a Backend Codebase

There we will turn the architecture we designed into a practical Java package structure using our Handler → UseCase → Repository convention, decide where domain and external Service code belongs, and establish the structure the project will use as Product, Inventory, Order, and Payment capabilities are implemented.