RizTech Academy logo
RizTech Academy
Writing Spring Worth ReadingLesson 3 of 525 min

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_SECRET set 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

  1. Move a group of app settings into a @ConfigurationProperties record and inject it; misconfigure one and confirm it fails at startup (type-safe).
  2. Create application-dev.properties and application-prod.properties with different ddl-auto and database settings; run with each profile active and confirm the right values load.
  3. Make a bean @Profile("dev") (a stub external client) and confirm it exists only in the dev profile.
  4. Read a value from application.properties, then override it with an environment variable, and confirm the env var wins.
  5. Reference a secret via ${DAKIYA_JWT_SECRET} and provide it as an environment variable; confirm no secret is in the committed file.
  6. Reason through the correct response to accidentally committing a real database password (rotate, not just delete).

Official documentation

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