Configuration and profiles, done right
The same Spring application runs on your laptop, in a test suite, on staging, and in production — with different configuration each time (different database, different log levels, different secrets). Managing that well is a small amount of discipline that prevents a class of bug and a class of breach. This lesson is how to handle configuration and profiles in Spring the right way.
Externalise configuration — never hard-code
The foundational rule (the twelve-factor principle): anything that differs between environments, or is secret, lives outside the code — in properties driven by the environment, not hard-coded. The database URL, credentials, the JWT secret, external service URLs, feature toggles, log levels — these vary or are sensitive, so they come from configuration, and the same build runs everywhere with different values fed in.
Spring reads configuration from application.properties (or .yml), and — crucially — those values can be
overridden by environment variables and command-line arguments, in a defined precedence order (env vars
and CLI args win over the file). So you put sensible defaults in application.properties and override the
per-environment and secret values from the environment in production. The code never changes between
environments; only the fed-in values do.
Type-safe configuration with @ConfigurationProperties
For your application's own settings, bind them to a typed object with @ConfigurationProperties rather than
sprinkling @Value("${...}") everywhere:
@ConfigurationProperties(prefix = "dakiya")
public record DakiyaProperties(
Duration parcelHoldTime,
int maxParcelsPerHub,
String partnerApiUrl
) {}
dakiya.parcel-hold-time=48h
dakiya.max-parcels-per-hub=500
dakiya.partner-api-url=https://partner.example.com
@ConfigurationProperties(prefix = "dakiya") binds the dakiya.* properties into the record — type-safe
(a Duration, an int, validated at startup), grouped, and injectable as a bean. This beats scattered
@Value("${dakiya.max-parcels-per-hub}") strings: the settings are in one typed place, misconfiguration
fails at startup, and there is no stringly-typed property key repeated across the codebase. Use
@ConfigurationProperties for a group of related settings; reserve @Value for the odd one-off.
Profiles: per-environment configuration
Profiles let you have different configuration per environment. Alongside application.properties
(shared defaults), add application-{profile}.properties:
application.properties # shared defaults
application-dev.properties # development overrides
application-prod.properties # production overrides
application-test.properties # test overrides
You activate a profile with SPRING_PROFILES_ACTIVE=prod (an environment variable) or
--spring.profiles.active=prod. Spring loads application.properties plus the active profile's file, with
the profile's values overriding. So application-prod.properties sets spring.jpa.hibernate.ddl-auto=validate
and a real database URL, while application-dev.properties uses update and a local database. You can also
make beans profile-specific with @Profile("dev") (a bean that only exists in development — a mock
external client, say). Profiles are how one codebase adapts cleanly to each environment without conditional
code.
Secrets: never in the repository
The security-critical rule, worth stating on its own: secrets never go in the committed configuration. The database password, the JWT signing key, API keys — committing any of these is a breach the moment it is pushed, and they live in git history forever (even if deleted later). They come from:
- Environment variables — the simplest:
DAKIYA_JWT_SECRETset in the deployment environment, read via${DAKIYA_JWT_SECRET}in a property. - A secrets manager — Vault, AWS Secrets Manager, or the platform's secret store — for production at scale.
application.properties in the repo holds non-secret defaults and references to secrets
(${DAKIYA_JWT_SECRET}), never the secret values. Commit an application.properties with placeholders and
document the required environment variables; never commit real credentials. If a secret is committed by
accident, the fix is to rotate it (change the credential), not just delete the commit — assume anything
pushed is compromised. This is the same discipline as every course: config and secrets from the environment,
never baked into the code.
Check your work
Externalise config. Anything that differs by environment or is secret lives outside the code, in properties overridable by environment variables / CLI args (which win over the file); the same build runs everywhere with different fed-in values.
Type-safe settings. @ConfigurationProperties(prefix=...) binds a group of settings to a typed object
(validated at startup, one place) — better than scattered @Value strings; @Value for one-offs.
Profiles. application-{profile}.properties overrides application.properties for the active profile
(SPRING_PROFILES_ACTIVE); @Profile makes beans environment-specific. One codebase, per-environment
config, no conditional code.
Secrets. Never in the committed config — from environment variables or a secrets manager; the repo holds placeholders/references, not values. A committed secret must be rotated, not just deleted.
Practice
- Move a group of app settings into a
@ConfigurationPropertiesrecord and inject it; misconfigure one and confirm it fails at startup (type-safe). - Create
application-dev.propertiesandapplication-prod.propertieswith differentddl-autoand database settings; run with each profile active and confirm the right values load. - Make a bean
@Profile("dev")(a stub external client) and confirm it exists only in the dev profile. - Read a value from
application.properties, then override it with an environment variable, and confirm the env var wins. - Reference a secret via
${DAKIYA_JWT_SECRET}and provide it as an environment variable; confirm no secret is in the committed file. - Reason through the correct response to accidentally committing a real database password (rotate, not just delete).
Official documentation
- Spring Boot — Externalized Configuration — Property sources and precedence.
- Spring Boot — @ConfigurationProperties — Type-safe config binding.
- Spring Boot — Profiles — Per-environment configuration.
Next: exceptions, logging and failing loudly.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship