Building the Spring Boot Application
Environment-Specific Configuration
আপনি একটি 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:
| Property | Purpose | Local Example |
|---|---|---|
spring.datasource.url | PostgreSQL connection | jdbc:postgresql://localhost:5432/order_management |
payment.base-url | Payment provider endpoint | http://localhost:9001 |
payment.timeout | Provider timeout | 2s |
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.ymlshould 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.
.envfiles 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 → Repositoryarchitecture, 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.