Spring Boot app.properties - Dev DB Wiped Profile
Staging connected to prod RDS due to hardcoded spring.profiles.active in base config.
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
- ✓Basic programming fundamentals
- ✓A computer with internet access
- ✓Willingness to follow along with examples
- application.properties is the externalized instruction sheet Spring Boot reads at startup — separate logic from environment without recompiling
- Spring loads properties in priority order: command-line args > OS env vars > profile-specific files > base application.properties
- @Value("${key:default}") injects single properties with a fallback — without the colon default, a missing key crashes the app at startup with no grace
- @ConfigurationProperties groups related settings into a type-safe POJO with Relaxed Binding — kebab-case, snake_case, and UPPER_SNAKE_CASE all map to camelCase fields automatically
- Profile files (application-prod.properties) overlay the base file — only conflicting keys are overridden, everything else is inherited
- Add @Validated to your @ConfigurationProperties class to fail-fast at startup with clear error messages when required properties are missing or malformed
- spring.config.import (Spring Boot 2.4+) replaces spring.config.additional-location for importing external config files and Vault/Config Server integration
- The biggest mistake: hardcoding spring.profiles.active=prod in the base file — your CI pipeline deploys prod settings everywhere and wipes test data
application.properties is Spring Boot's default configuration file — a key-value store that externalizes every tunable knob in your application: database URLs, server ports, feature flags, thread pools, and credentials. It lives at src/main/resources/application.properties by default, but Spring Boot searches multiple locations in a defined order (classpath root, config/ subdirectory, current directory, and SPRING_CONFIG_LOCATION environment variable), letting you override defaults without touching the JAR.
This file exists because hardcoding configuration kills portability; you need one artifact that behaves differently in dev, staging, and prod without recompiling.
Spring Boot profiles (spring.profiles.active) let you split configuration into application-{profile}.properties files — e.g., application-dev.properties with a wiped H2 database and application-prod.properties pointing to a real PostgreSQL cluster. The @Value annotation injects individual properties into fields, while @ConfigurationProperties binds entire property groups to typed POJOs with validation.
Prefer @ConfigurationProperties for anything beyond a single scalar — it catches typos at startup and keeps your code clean.
application.yml is the YAML alternative: hierarchical, less repetitive for nested configs, but whitespace-sensitive and prone to silent failures when indentation breaks. Properties files are flatter and more forgiving — pick YAML if you have deep nesting (e.g., multi-datasource configs), stick with .properties for simplicity and tooling compatibility.
Neither is suitable for secrets: credentials belong in environment variables, HashiCorp Vault, or cloud secret managers (AWS Secrets Manager, GCP Secret Manager), not in version control.
For production, externalize configuration entirely: set spring.config.location or SPRING_CONFIG_LOCATION to a directory outside the JAR, use --spring.config.additional-location for layered overrides, and never rely on baked-in defaults for sensitive environments. Spring Cloud Config or Kubernetes ConfigMaps can centralize this further, but the principle is the same — your JAR is immutable, configuration is dynamic.
Imagine your app is a new employee on their first day. They need to know which office building to report to, what their login credentials are, and how many minutes they get for lunch. The application.properties file is exactly that — the instruction sheet you hand your Spring Boot app before it starts work. Instead of baking those instructions permanently into the code, you write them in one tidy file and the app reads them at startup. Change the file, change the behavior — no rewriting code required.
The part most introductions skip: you can have different instruction sheets for different situations. The base sheet covers everything that is always true. A separate prod sheet covers what changes in production. The app reads the base sheet first, then the environment-specific sheet overrides only the lines that differ. And if an instruction arrives via a system environment variable — say, a database password injected by Kubernetes at runtime — that always wins over anything in any file. Understanding that priority chain is the difference between a developer who uses Spring Boot and one who can debug it at 2 AM.
In professional software engineering, hardcoding is a cardinal sin. Embedding your database password or server port directly into Java source code creates a fragile system that requires a full recompile just to change a timeout value. This is where externalized configuration becomes critical.
Spring Boot's application.properties serves as the control center for your entire application. It separates the logic of your code from the environment it runs in. Whether deploying to a local Docker container or a Kubernetes cluster, the code stays the same — only the properties change.
Misconfiguring properties causes production failures that are silent and genuinely hard to diagnose. A misplaced space around the equals sign silently ignores your config change. A missing environment variable crashes the app at 2 AM when the fallback was never defined. Using tabs instead of spaces in a YAML file misparses your entire datasource configuration without throwing a startup error. These are not theoretical edge cases — they are the class of incidents that generate post-mortems.
This guide covers property injection patterns, environment profiles, the fail-fast validation strategy that catches misconfiguration at startup rather than at runtime, and the configuration management approaches used in production systems that handle secrets without committing them to version control.
What application.properties Actually Is and Where It Lives
Spring Boot follows a strict priority order for loading configuration. By default, it looks in src/main/resources/application.properties. When your build tool packages your app, this file is moved to the root of the classpath inside your JAR. Spring Boot finds it by convention — you do not register it anywhere.
The syntax is a flat key=value format. Spring uses these keys to auto-configure beans. Defining spring.datasource.url does not just store a string — it triggers Spring Boot's auto-configuration to create a DataSource bean, a HikariCP connection pool, and wire the entire JPA stack. You provide the parameters; Spring Boot writes the plumbing.
The priority chain matters more than most developers realize. A value in application.properties can be overridden by application-prod.properties, which can be overridden by an OS environment variable, which can be overridden by a JVM argument, which can be overridden by a command-line argument. Every environment variable your Kubernetes pod injects wins over anything in any file. Understanding this chain is what lets you debug configuration mismatches in under five minutes instead of four hours.
One critical detail the official docs bury: as of Spring Boot 2.4, the way additional config files are imported changed. The old spring.config.additional-location property still works, but the new spring.config.import is the forward-compatible approach and is required for Spring Cloud Config Server and Vault integration.
# ── io.thecodeforge: Standard Server Config ─────────────────────── server.port=8081 server.servlet.context-path=/api/v1 # ── Application Identity ────────────────────────────────────────── spring.application.name=forge-bookstore-service # ── Database Connection (PostgreSQL) ────────────────────────────── # ${DB_PASSWORD} resolves from OS environment variable. # The :default_dev_pass fallback only applies locally — never in prod. # In prod, DB_PASSWORD must be set explicitly or startup fails at datasource creation. spring.datasource.url=jdbc:postgresql://localhost:5432/forge_db spring.datasource.username=forge_admin spring.datasource.password=${DB_PASSWORD:default_dev_pass} spring.datasource.driver-class-name=org.postgresql.Driver # ── Connection Pool (HikariCP) ──────────────────────────────────── # These defaults are tuned for a small service — adjust for your load profile. # Too small: connection starvation under load. # Too large: DB server thread exhaustion. spring.datasource.hikari.maximum-pool-size=10 spring.datasource.hikari.minimum-idle=2 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.idle-timeout=600000 # ── JPA / Hibernate ─────────────────────────────────────────────── spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect # validate: checks schema matches entities at startup — use in prod # update: modifies schema automatically — never use in prod # none: no schema management — use in prod with external migrations (Flyway/Liquibase) spring.jpa.hibernate.ddl-auto=validate # ── Logging Configuration ───────────────────────────────────────── logging.level.root=INFO logging.level.io.thecodeforge=DEBUG # ── Importing Additional Config (Spring Boot 2.4+) ──────────────── # Use spring.config.import to pull in extra config files or external sources. # For Vault: spring.config.import=vault:// # For Config Server: spring.config.import=configserver: # For local extra file: spring.config.import=optional:classpath:extra-config.properties # 'optional:' prefix means do not fail if the file does not exist. # spring.config.import=optional:classpath:extra-config.properties
Injecting Properties Into Java Classes: @Value vs @ConfigurationProperties
Spring provides two mechanisms for getting property values into your Java code. @Value is the quick option for a single value. @ConfigurationProperties is the production option for any group of related settings.
@Value("${key:default}") injects a single property directly into a field. The colon-default syntax is not optional in production — without it, a missing property crashes the application at startup with IllegalArgumentException: Could not resolve placeholder. That crash at 2 AM during deployment is avoidable.
@ConfigurationProperties binds an entire property prefix to a typed Java class. It supports Relaxed Binding, which means forge.bookstore.page-size in the properties file maps to private int pageSize in your Java class automatically — no exact key matching required. It also supports JSR-303 validation via @Validated, which gives you explicit startup failures with clear messages when required properties are missing or out of range.
The IDE tooling difference is real and worth mentioning: if you add the spring-boot-configuration-processor dependency to your build, IntelliJ and VS Code generate autocomplete suggestions, type hints, and deprecation warnings for every field in your @ConfigurationProperties class. Without the processor, you are typing property keys blind. With it, you get the same IDE experience for custom properties that you get for Spring's built-in ones.
package io.thecodeforge.bookstore.config; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; import lombok.Data; /** * io.thecodeforge: Type-safe configuration with validation. * * @Validated: activates JSR-303 validation at startup. * If any constraint fails, the app refuses to start with a clear error message. * This is the fail-fast pattern — catch misconfiguration at startup, not at runtime. * * Relaxed Binding examples (all resolve to the same field): * forge.bookstore.page-size=50 (kebab-case in .properties) * forge.bookstore.page_size=50 (snake_case) * FORGE_BOOKSTORE_PAGE_SIZE=50 (env var / UPPER_SNAKE_CASE) * forge.bookstore.pageSize=50 (camelCase) */ @Data @Component @Validated @ConfigurationProperties(prefix = "forge.bookstore") public class ForgeBookstoreProperties { // Default values apply when the property is not defined in any config file. // These should be safe, conservative defaults — not production tuning values. @Min(value = 1, message = "forge.bookstore.page-size must be at least 1") private int pageSize = 10; @NotBlank(message = "forge.bookstore.default-currency must not be blank") private String defaultCurrency = "USD"; // @Valid cascades validation into nested configuration objects. // Without @Valid here, the constraints inside Security are never evaluated. @Valid @NotNull(message = "forge.bookstore.security configuration block is required") private Security security = new Security(); @Data public static class Security { // App refuses to start if forge.bookstore.security.api-key is missing or blank. // No more null API keys silently reaching production. @NotBlank(message = "forge.bookstore.security.api-key must be configured") private String apiKey; @Min(value = 5, message = "Session timeout must be at least 5 minutes") private int sessionTimeoutMinutes = 30; // Pattern validation — api-key must match expected format // Uncomment when you have a known format to enforce: // @Pattern(regexp = "^[A-Za-z0-9\\-]{32,64}$", // message = "forge.bookstore.security.api-key must be 32-64 alphanumeric chars") } }
Spring Boot Profiles — Environment-Specific Configuration Without Code Changes
Every application runs in multiple environments: your laptop, a CI runner, staging, and production. The database URLs differ. The log levels differ. The external service endpoints differ. Profiles let you express those differences in configuration files without touching the Java code or maintaining separate build artifacts.
Spring Boot loads files in this order: application.properties first as the baseline, then application-{profile}.properties as the overlay. Only keys that appear in both files are overridden — everything in the base file that is not mentioned in the profile file is inherited as-is. This means your profile files should be small and specific to what actually changes per environment.
Multiple profiles can be active simultaneously. SPRING_PROFILES_ACTIVE=prod,monitoring activates both the prod profile and a monitoring profile. Spring loads both overlay files. If the same key appears in both profile files, the last one wins in alphabetical order — which is why relying on multiple overlapping profiles for the same key is a maintenance trap.
The profile activation question is one of the most consistently asked in Spring Boot interviews: if the same property key appears in application.properties, application-prod.properties, AND an OS environment variable, what wins? The environment variable wins unconditionally. The full priority chain from highest to lowest is: command-line arguments, Java system properties (-D flags), OS environment variables, profile-specific property files, base application.properties.
# ── io.thecodeforge: Production Overrides ───────────────────────── # This file contains ONLY what differs in production. # Everything not listed here is inherited from application.properties. # Keeping this file small is a feature — it makes prod differences explicit. # Production runs on standard HTTPS port behind a load balancer server.port=8080 # Production RDS instance — URL comes from environment variable for security # SPRING_DATASOURCE_URL is set in the Kubernetes Secret and injected as env var spring.datasource.url=${SPRING_DATASOURCE_URL} spring.datasource.username=${SPRING_DATASOURCE_USERNAME} spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} # Production connection pool — sized for actual production load spring.datasource.hikari.maximum-pool-size=25 spring.datasource.hikari.minimum-idle=5 # Schema validation (not creation) — production schema is managed by Flyway spring.jpa.hibernate.ddl-auto=validate # No SQL logging in production — performance cost and log volume are not acceptable logging.level.root=WARN logging.level.io.thecodeforge=INFO spring.jpa.show-sql=false # Stricter session timeout in production forge.bookstore.security.session-timeout-minutes=15
application.properties vs application.yml — Syntax, Trade-offs, and Failure Modes
Both formats configure the same Spring Boot beans. The properties file is a flat key=value list. The YAML file is a hierarchical structure that eliminates prefix repetition at the cost of indentation sensitivity. Neither is objectively better — each has a specific failure mode that the other does not.
The properties file failure mode: almost none. A trailing space on a key makes it unrecognizable, but the app starts and you notice the value is wrong. A missing equals sign causes a parse error with a clear line number. It is hard to silently corrupt a properties file.
The YAML failure mode: silent misconfiguration. A tab character instead of two spaces causes the YAML parser to misparse the structure. Properties end up under the wrong parent key. Spring may create a bean with a null URL. The app starts, no parse error is thrown, and every database call fails at runtime. This class of failure is significantly harder to diagnose than a startup crash.
YAML has one genuine superpower: multi-document support. Using --- as a separator, you can define multiple Spring profiles in a single application.yml file. In Spring Boot 2.4 and later, use spring.config.activate.on-profile inside the document block. This is a YAML-only feature — you cannot do it with properties files.
The practical advice: use properties files for simple, flat configuration where correctness matters more than readability. Use YAML for complex nested configuration in microservices where the visual hierarchy genuinely helps. If you choose YAML, add a CI linting step that catches tab characters before they reach any server.
# io.thecodeforge: YAML configuration reference # Equivalent to the application.properties example above, plus multi-profile demo. spring: application: name: forge-bookstore-service datasource: url: jdbc:postgresql://localhost:5432/forge_db username: forge_admin # Environment variable resolution works identically in YAML password: ${DB_PASSWORD:default_dev_pass} hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 jpa: hibernate: ddl-auto: validate database-platform: org.hibernate.dialect.PostgreSQLDialect show-sql: false server: port: 8081 servlet: context-path: /api/v1 logging: level: root: INFO io.thecodeforge: DEBUG forge: bookstore: page-size: 10 default-currency: USD security: session-timeout-minutes: 30 # ── Multi-document YAML: profile-specific overrides in one file ──── # The --- separator starts a new YAML document. # spring.config.activate.on-profile is the Spring Boot 2.4+ syntax. # Older spring.profiles syntax is deprecated as of 2.4 — do not use it. --- spring: config: activate: on-profile: dev server: port: 9090 logging: level: root: DEBUG org.hibernate.SQL: TRACE --- spring: config: activate: on-profile: prod server: port: 8080 spring: datasource: url: ${SPRING_DATASOURCE_URL} hikari: maximum-pool-size: 25 logging: level: root: WARN
Secrets Management and External Configuration: Beyond the Properties File
For applications beyond the hobby scale, storing secrets in properties files — even with environment variable placeholders — is not sufficient. Rotating a database password means updating an environment variable on every host, restarting every instance, and hoping nothing was cached. A secret manager like HashiCorp Vault, AWS Secrets Manager, or Spring Cloud Config Server provides centralized secret storage, automatic rotation, audit logs, and fine-grained access control.
Spring Boot 2.4 introduced spring.config.import as the standardized way to pull configuration from external sources. A single line in application.properties or application.yml pulls the entire secret set from Vault or Config Server at startup. If the external source is unreachable and the import is not marked optional, the application refuses to start — which is exactly the fail-fast behavior you want rather than starting with missing secrets.
For teams not ready for Vault or Config Server, Jasypt (Java Simplified Encryption) provides a middle ground: properties files that store AES-encrypted values instead of plaintext. The encryption key is injected at runtime via environment variable. This is not equivalent security to Vault, but it prevents credentials from being readable in any file committed to version control or stored on disk in plaintext.
The environment variable approach used throughout this guide is the minimum acceptable standard for production. The approaches in this section are what teams with actual security requirements implement after the environment variable baseline is working.
# ── io.thecodeforge: External Configuration Import Examples ─────── # --- Spring Cloud Config Server --- # Imports all properties from Config Server at startup. # Config Server URL is typically set via SPRING_CONFIG_URI env var. # If Config Server is unreachable, startup fails immediately (fail-fast). spring.config.import=configserver: # --- HashiCorp Vault --- # Imports secrets from Vault path secret/forge-bookstore. # Requires spring-cloud-starter-vault-config dependency. # spring.config.import=vault:// # spring.cloud.vault.uri=http://vault.internal:8200 # spring.cloud.vault.token=${VAULT_TOKEN} # spring.cloud.vault.kv.enabled=true # spring.cloud.vault.kv.default-context=forge-bookstore # --- Optional file import --- # Import an additional file if it exists — do not fail if absent. # Useful for local developer overrides that are .gitignored. # spring.config.import=optional:file:./local-override.properties # --- AWS Parameter Store (Spring Cloud AWS) --- # spring.config.import=aws-parameterstore: # spring.cloud.aws.parameterstore.enabled=true # ── Jasypt Encryption (middle-ground secret management) ─────────── # Encrypted values are stored as ENC(encryptedString). # The encryption key is injected at runtime — never stored in the file. # jasypt.encryptor.password=${JASYPT_ENCRYPTOR_PASSWORD} # # Example encrypted datasource password: # spring.datasource.password=ENC(Gb3V+Kj7Mc2VzXP4q8Rn5A==) # Jasypt decrypts this at startup using the key from JASYPT_ENCRYPTOR_PASSWORD. # Without the key, the encrypted string is meaningless — safe to commit.
Externalize the Configuration of SpringApplication — Because Production Is Not a Sandbox
Spring Boot gives you a SpringApplication object. Most devs treat it like a black box. They don't. That object is your last line of defense before your app hits production. Configure it externally.
The spring.config.name and spring.config.location environment variables let you swap entire configuration directories without touching a single line of code. No rebuild. No redeploy. Just a JVM flag.
Why should you care? Because I've seen teams hardcode file paths in application.properties, then panic when their Kubernetes pod can't find the config volume. Or worse, they embed secrets in the JAR. Externalize the SpringApplication configuration and sleep better at night.
Here's the pattern: set SPRING_CONFIG_NAME=myapp and SPRING_CONFIG_LOCATION=file:/etc/config/ on the host. Spring Boot loads /etc/config/myapp.properties. If the file is missing, it fails fast. You want that. Fail fast, not fail silently at 3 AM.
// io.thecodeforge — java tutorial import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.core.env.Environment; @SpringBootApplication public class PaymentProcessorApplication { public static void main(String[] args) { // Environment variable overrides built-in defaults // Set SPRING_CONFIG_NAME=payment or use --spring.config.name=payment SpringApplication app = new SpringApplication(PaymentProcessorApplication.class); // External path: /opt/payment/config/ // Spring Boot reads /opt/payment/config/payment.properties app.setDefaultProperties(java.util.Map.of( "spring.config.name", System.getenv().getOrDefault("SPRING_CONFIG_NAME", "application"), "spring.config.location", System.getenv().getOrDefault("SPRING_CONFIG_LOCATION", "classpath:/") )); Environment env = app.run(args).getEnvironment(); System.out.println("Active config file: " + env.getProperty("spring.config.name")); } }
spring.config.location in your properties file. That defeats externalisation. Always set it via environment variable or command line. If your CI/CD pipeline doesn't inject it, your config is broken.spring.config.name and spring.config.location via environment variables. Never embed config paths in the JAR.Automatically Expand Properties at Build Time — Stop Duplicating Values Across pom.xml and application.properties
You have a Maven version in pom.xml and the same version in application.properties. You update one, forget the other, and spend an hour debugging a version mismatch. This is stupid. Spring Boot Maven and Gradle plugins let you expand build properties directly into your configuration files at compile time. No duplication.
With Maven, use @project.version@, @project.build.sourceEncoding@, or any Maven property. Spring Boot's spring-boot-starter-parent enables resource filtering by default. Add the placeholder in your properties file, and Maven replaces it during the process-resources phase.
If you don't use the parent POM, you must manually enable filtering in your pom.xml. Yes, it's boilerplate. Yes, it's worth it. The alternative is chasing configuration drift across three environments.
For Gradle, the spring-boot-gradle-plugin does the same with $ placeholders. Same rule: keep your build config as the single source of truth.
// io.thecodeforge — java tutorial // application.properties — before build app.version=@project.version@ app.build.timestamp=@timestamp@ app.java.version=@java.version@ // pom.xml — without spring-boot-starter-parent, you need this: <build> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.3.1</version> </plugin> </plugins> </build> // After build — application.properties contains: app.version=2.5.1 app.build.timestamp=2024-09-28T10:30:00Z app.java.version=17
spring-boot:run with addResources=true), resource filtering is bypassed. Use exec:java instead, or set filtering directly on src/main/resources.@..@ or Gradle $ placeholders in properties files to keep build configuration the single source of truth. No manual duplication.Discover Built-in Options for External Properties — Because Guessing Isn't a Strategy
Spring Boot doesn't just read application.properties from your classpath and call it a day. It has a defined, overridable search path that spans multiple locations — and you need to know it before production eats your config. The default order starts at the classpath, then moves to the current directory's /config subdirectory, then the current directory root, then /config directly under the classpath root. Each subsequent location overrides the previous one. You can override this entirely with spring.config.location or append extra paths with spring.config.additional-location. Why does this matter? Because ops teams routinely drop overrides in /config/ outside the JAR. If you don't know the search order, you'll debug phantom values at 2 AM. The real power is combining this with spring.config.import — you can pull properties from HTTP URLs, Vault, or even Git. Spring Cloud Config? That's just a plugin. The core boot engine already supports external property sources out of the box. Stop hardcoding paths you'll regret later.
// io.thecodeforge — java tutorial // Show how Spring Boot resolves config from multiple locations import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.core.env.Environment; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; @SpringBootApplication public class ExternalConfigDemo implements CommandLineRunner { @Autowired private Environment env; public static void main(String[] args) { // Override order: classpath, file:./config/, file:./ SpringApplication app = new SpringApplication(ExternalConfigDemo.class); app.setDefaultProperties(java.util.Map.of("spring.config.name", "custom")); app.run(); } @Override public void run(String... args) { String dbUrl = env.getProperty("database.url"); System.out.println("Resolved database URL: " + dbUrl); } }
spring.config.location, it completely replaces the default search order — no fallback to classpath. Use spring.config.additional-location to append, not replace.Properties in Parent-Child Contexts — The Override You Didn't Know Was There
When you run a Spring Boot app that loads a parent context (like a traditional WAR in a servlet container, or a context hierarchy via Spring Cloud), properties don't flow the way you expect. The parent context's Environment is NOT visible to child beans by default. Child contexts have their own property sources. If you define a property in the parent's application.properties, don't assume your REST controller in the child sees it. This bites people migrating from monolithic Spring MVC apps to Boot with embedded containers. The fix: use spring.config.use-legacy-processing=false in the child? No — that's a boot 2.4+ change for file loading, but doesn't fix context isolation. The real solution is to pass properties manually or use a shared PropertySourcesPlaceholderConfigurer that bridges contexts. Or better, avoid parent-child entirely: Boot's convention is single-context. If you must nest, inject the parent's Environment into the child via a factory bean. Otherwise, you'll waste hours wondering why @Value("${my.property}") returns null in a child context while it works in the parent.
// io.thecodeforge — java tutorial // Bridge parent properties to child context import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; @Configuration public class ParentChildConfigFix { @Bean public static PropertyPlaceholderConfigurer childProps(Environment parentEnv) { PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); ppc.setLocation(new ClassPathResource("application.properties")); // Fallback to parent env for unresolvable properties ppc.setProperties(parentEnv.getSystemProperties()); ppc.setIgnoreUnresolvablePlaceholders(true); return ppc; } }
The Dev Database Wipe — Hardcoded Profile in Base Config
- Never set spring.profiles.active in the base application.properties — it overrides environment-level control and affects every environment that loads the base file
- Set profiles exclusively via environment variables (SPRING_PROFILES_ACTIVE) or JVM arguments (-Dspring.profiles.active=prod)
- Add startup validation that logs the active datasource URL and fails loudly if a production URL appears on a non-production host
- The base file should contain only environment-agnostic defaults that are safe to activate in any environment
curl -s http://localhost:8080/actuator/env | jq '.propertySources[] | select(.name | contains("application"))'curl -s http://localhost:8080/actuator/env/server.port | jq .curl -s http://localhost:8080/actuator/env | jq '.activeProfiles'printenv SPRING_PROFILES_ACTIVEcat -A src/main/resources/application.yml | grep '^I'python3 -c "import yaml; yaml.safe_load(open('src/main/resources/application.yml'))"curl -s http://localhost:8080/actuator/env/spring.datasource.url | jq '.property'curl -s http://localhost:8080/actuator/health/db | jq .| Feature / Aspect | application.properties | application.yml |
|---|---|---|
| Format style | Flat key=value pairs, one per line. spring.datasource.url=... repeated for every datasource key. | Indented YAML hierarchy. spring.datasource is written once; url, username, password nest beneath it. |
| Readability — simple config | Excellent — the format is immediately obvious to any developer regardless of YAML experience. | Slightly more verbose for simple flat configs — the hierarchy adds keystrokes without adding clarity. |
| Readability — nested config | Gets repetitive fast. Five datasource keys means writing spring.datasource five times. | Excellent — hierarchy is visual and matches the logical grouping of the settings. |
| Multiple profiles in one file | Not possible — you must use separate files: application-dev.properties, application-prod.properties. | Yes, using the --- document separator with spring.config.activate.on-profile (Spring Boot 2.4+ syntax). |
| Risk of silent misconfiguration | Very low — trailing space on key is detectable. Parse errors report a clear line number. | Real risk — a tab character silently misparses the structure. Properties end up under wrong parent keys with no startup error. |
| Lists and arrays | Indexed: my.list[0]=a, my.list[1]=b. Comma-separated: my.list=a,b,c also works for simple lists. | Clean YAML list syntax: use hyphen-prefixed items under the key. More readable for multi-item lists. |
| IDE autocomplete support | Full support in IntelliJ and VS Code with spring-boot-configuration-processor on the classpath. | Full support in IntelliJ and VS Code with spring-boot-configuration-processor on the classpath. |
| Default in Spring Initializr | Yes — generated by default when creating a new project. | Optional — select during project setup on start.spring.io. |
| Recommended for beginners | Yes — simpler syntax and far harder to break silently. | Better once you understand indentation rules and have a tab-detection step in your workflow. |
| Spring Boot 2.4+ multi-profile syntax | Not applicable — profile activation is always per-file. | Use spring.config.activate.on-profile. The old spring.profiles syntax is deprecated in 2.4 and removed in Spring Boot 3.0. |
| Team preference trend | Common in mature codebases and teams prioritizing configuration correctness over visual hierarchy. | Increasingly preferred in new microservice projects where nested configuration is the norm. |
| File | Command / Code | Purpose |
|---|---|---|
| src | server.port=8081 | What application.properties Actually Is and Where It Lives |
| io | /** | Injecting Properties Into Java Classes |
| src | server.port=8080 | Spring Boot Profiles |
| src | spring: | application.properties vs application.yml |
| src | spring.config.import=configserver: | Secrets Management and External Configuration |
| SpringApplicationExternalConfig.java | @SpringBootApplication | Externalize the Configuration of SpringApplication |
| BuildTimeExpansion.java | app.version=@project.version@ | Automatically Expand Properties at Build Time |
| ExternalConfigDemo.java | @SpringBootApplication | Discover Built-in Options for External Properties |
| ParentChildConfigFix.java | @Configuration | Properties in Parent-Child Contexts |
Key takeaways
Common mistakes to avoid
8 patternsPutting a trailing space on the key side of the equals sign
Forgetting to enable @ConfigurationProperties scanning
Hardcoding spring.profiles.active=prod in the base application.properties
Using @Value without a colon-default fallback
Mixing application.properties and application.yml in the same project
Missing spring-boot-configuration-processor dependency
Using spring.profiles syntax in multi-document YAML (deprecated in 2.4, removed in 3.0)
Marking external configuration imports as optional in production
Interview Questions on This Topic
What is the difference between @Value and @ConfigurationProperties? When would you choose one over the other?
How does Spring Boot resolve properties? If the same key is in application.properties and a system environment variable, which one takes priority?
A Spring Boot application is not picking up changes in application-prod.properties. What are the first three things you would check?
Explain Relaxed Binding in Spring Boot and give an example of how it applies to configuration properties.
How can you use @Validated with @ConfigurationProperties to ensure your application fails to start if a required property is missing or malformed?
What changed about external configuration loading in Spring Boot 2.4, and what is the difference between spring.config.import and spring.config.additional-location?
Frequently Asked Questions
It goes in src/main/resources. When Maven or Gradle builds the project, everything in src/main/resources is copied to the root of the output JAR. Spring Boot knows to look there by convention — you do not register the file anywhere. If you need a developer-local override that should not be committed to Git, you can create application-local.properties in the same directory, add it to .gitignore, and activate it with SPRING_PROFILES_ACTIVE=local. Only properties that differ locally need to be in that file.
Technically yes, Spring Boot loads both. If the same key appears in both files, the .properties file takes precedence over .yml. This precedence is non-obvious and creates a class of debugging problems where a YAML change has no effect because the properties file is silently overriding it. Pick one format for your project, delete the other, and document the choice in your project README. The only exception is during a migration from one format to the other — but that migration should be a single commit that removes the old format entirely.
Use ${VARIABLE_NAME} syntax directly: spring.datasource.url=${DB_URL}. Spring resolves this against OS environment variables at startup. You can add a fallback for local development: spring.datasource.url=${DB_URL:jdbc:h2:mem:testdb}. The fallback after the colon is used when the environment variable is not set. In production, the environment variable should always be set — if DB_URL is absent and you have no fallback, the application fails at startup, which is the correct behavior for a missing production credential.
Yes. Enable /actuator/env in your exposure list and hit the endpoint. It shows every resolved property value and, critically, which source provided it — applicationConfig:[prod], systemEnvironment, commandLineArgs, and so on. This is the fastest way to diagnose configuration mismatches: if the value is wrong, the source field tells you exactly which configuration layer to fix. Secure this endpoint with ADMIN role in your SecurityFilterChain — it can expose database URLs and other sensitive configuration paths to anyone who can reach it.
YAML requires spaces for indentation — tabs are not valid YAML syntax. A tab character causes the YAML parser to misparse the document structure. Depending on where the tab appears, properties end up under the wrong parent key, are silently ignored, or the entire document fails to parse. The particularly dangerous outcome is when the parser does not throw an error — the application starts, the datasource URL is null under the wrong key, and every database call fails at runtime rather than at startup. Always use 2-space indentation and run cat -A application.yml | grep '^I' to detect tabs before committing.
Three ways, from most to least preferred: (1) Set the environment variable SPRING_PROFILES_ACTIVE=prod on the server, in Docker via -e, or in Kubernetes via env in the pod spec — this is the standard for production deployments. (2) Pass it as a JVM argument: java -Dspring.profiles.active=prod -jar app.jar — useful for local testing of profile behavior. (3) Pass it as a command-line argument: java -jar app.jar --spring.profiles.active=prod — highest priority, overrides everything. Never set spring.profiles.active in the base application.properties file — it short-circuits environment variable control and affects every environment that loads the base file.
spring.config.additional-location (pre-2.4) loads additional file-based configuration sources and adds them to the property source list. It works only with files and classpath resources. spring.config.import (Spring Boot 2.4+) is the replacement and works with files, URL sources, and registered ConfigDataLoader implementations — including Spring Cloud Config Server (configserver:), HashiCorp Vault (vault://), and AWS Parameter Store. For any integration with external secret managers or configuration servers, spring.config.import is required. The optional: prefix controls fail-fast behavior: without it, startup fails if the source is unreachable — which is the correct production behavior.
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
That's Spring Boot. Mark it forged?
7 min read · try the examples if you haven't