If you've spent time with Spring Boot, you know the spring.profiles.active routine — one property, multiple application-{profile}.properties files, and a creeping suspicion that somewhere a prod config is silently falling back to dev defaults.
Solon's answer is solon.env. Same concept, different design choices — and one pattern (solon.env.on in-file segments) that Spring has no direct equivalent for.
Here's how it works.
The Single Key: solon.env
Everything in Solon's multi-environment system pivots on one property:
# app.yml
solon.env: dev
Set this, and Solon automatically loads app-dev.yml alongside app.yml. Remove it, and only app.yml loads. No profiles API, no @ActiveProfiles, no annotation scanning — just a key.
Environment-Specific Files
The naming convention is app-{env}.yml (or .properties):
app.yml ← always loaded
app-dev.yml ← loaded when solon.env=dev
app-pro.yml ← loaded when solon.env=pro
app-test.yml ← loaded when solon.env=test
Keys in app-{env}.yml override the same keys in app.yml. Everything else in app.yml is inherited.
One constraint worth knowing: app-{env}.yml cannot itself declare another solon.env. The env chain is intentionally one level deep.
Four Ways to Activate an Environment
You don't have to hardcode solon.env in the file. You can set it at any of four levels, with higher numbers winning:
| Priority | Method | Example |
|---|---|---|
| 1 (lowest) | In app.yml
|
solon.env: dev |
| 2 | JVM system property | -Dsolon.env=pro |
| 3 | Launch argument | --env=pro |
| 4 (highest) | OS environment variable | export solon.env=pro |
In practice: set solon.env: dev in app.yml as a safe default for local development, then override at deploy time:
# Jar deployment
java -jar demo.jar --env=pro
# Docker run
docker run -e 'solon.env=pro' demo_image
# Kubernetes pod spec
env:
- name: solon.env
value: pro
All three launch forms below are equivalent:
java -Dsolon.env=pro -jar demo.jar
java -jar demo.jar --solon.env=pro
java -jar demo.jar --env=pro
Six Loading Tiers
solon.env controls which env file loads, but that's just the first two tiers of a six-tier config loading stack. Later tiers override earlier ones:
| Tier | Source | Notes |
|---|---|---|
| 1 |
app.yml + app-{env}.yml
|
Base config, inside the jar |
| 2 |
solon.config.load classpath entries |
v2.2.7+, supports ${solon.env} variables; wildcard * from v2.7.6+ |
| 3 |
solon.config.add external files |
Files placed beside the jar at runtime |
| 4 | JVM props / env vars / launch args | Runtime overrides |
| 5 |
app.cfg().loadAdd() in startup code |
Programmatic additions |
| 6 | Solon Cloud Config (e.g., Nacos) | Remote config center |
The pattern: static defaults live inside the jar, runtime overrides live outside it, remote config sits at the top.
Loading More Configs: solon.config.load
For apps with per-module configs, solon.config.load lets you declare additional classpath resources with variable substitution and wildcard support (v2.7.6+):
# app.yml
solon.env: dev
solon.config.load:
- "classpath:${solon.env}/jdbc.yml" # resolves to classpath:dev/jdbc.yml
- "classpath:${solon.env}/*.yml" # all yml in dev/ folder (v2.7.6+)
- "classpath:app-ds-${solon.env}.yml" # e.g., app-ds-dev.yml
- "classpath:common/base.yml" # environment-agnostic shared config
This lets you split config by concern (database, cache, auth) rather than cramming everything into one file per environment.
For external files placed beside the jar, use solon.config.add:
solon.config.add: "./local-overrides.yml"
Or at launch time:
java -jar demo.jar --solon.config.add=./local-overrides.yml
The In-File Segment Trick (v2.5.5+)
This is the feature that has no Spring Boot equivalent. Solon lets you embed multiple environment-specific segments inside a single app.yml, separated by --- and guarded by solon.env.on:
solon.env: pro
---
solon.env.on: pro
demo.auth:
user: root
password: "${AUTH_PASSWORD}"
---
solon.env.on: dev|test
demo.auth:
user: demo
password: "demo1234"
Only the segment matching the current solon.env is applied. The | syntax lets one segment cover multiple environments.
This pattern suits small services where maintaining separate env files feels heavy, but you still want clear boundaries in the source.
Programmatic Loading
Sometimes you need to load config based on conditions known only at startup. Two clean hooks:
@SolonMain
public class App {
public static void main(String[] args) {
Solon.start(App.class, args, app -> {
// Tier 5: added before beans initialize, merged into the config stack
app.cfg().loadAdd("app-jdbc-" + app.cfg().env() + ".yml");
app.cfg().loadAdd("app-cache-" + app.cfg().env() + ".yml");
});
}
}
Or via @Import on the startup class:
@Import(profiles = "classpath:module-defaults.yml")
@SolonMain
public class App {
public static void main(String[] args) {
Solon.start(App.class, args);
}
}
For Docker/Kubernetes, loadEnv() pulls in environment variables by prefix:
Solon.start(App.class, args, app -> {
app.cfg().loadEnv("demo."); // pulls in all env vars starting with "demo."
});
Note: Solon automatically syncs solon.* environment variables into the config store, so solon.env=pro as a container env var works without loadEnv().
Injecting Config Values
@Inject with ${} syntax wires config values into beans:
@Component
public class DataSourceConfig {
@Inject("${db.url}")
private String dbUrl;
@Inject("${db.username}")
private String username;
}
For structured config objects, @BindProps binds an entire prefix:
@BindProps(prefix = "app.mail")
@Component
public class MailProperties {
public String host;
public int port;
public String username;
}
Variable references also work inside YAML values themselves:
db.server: "10.0.0.1"
db.name: "orders"
db.url: "jdbc:mysql://${db.server}/${db.name}" # composed from other keys
A Realistic Project Layout
src/main/resources/
├── app.yml # base config + solon.env: dev
├── app-dev.yml # dev overrides
├── app-pro.yml # prod overrides
└── config/
├── common/
│ └── base.yml # shared constants (env-agnostic)
├── dev/
│ └── jdbc.yml # dev DB connection
└── pro/
└── jdbc.yml # prod DB connection
app.yml:
solon.env: dev
solon.config.load:
- "classpath:config/common/base.yml"
- "classpath:config/${solon.env}/jdbc.yml"
solon.app.name: "order-service"
server.port: 8080
app-dev.yml:
solon.logging.level: debug
solon.debug: 1
app-pro.yml:
solon.logging.level: warn
server.port: 80
Deploy to prod:
java -jar order-service.jar --env=pro
One flag flips the entire config stack.
Migration Reference (from Spring Boot)
| Spring Boot | Solon | Notes |
|---|---|---|
spring.profiles.active |
solon.env |
Same concept, different key |
application-{profile}.properties |
app-{env}.yml |
Same convention |
@Profile("dev") on beans |
solon.env.on: dev in YAML |
Config-layer isolation vs code-layer |
@PropertySource |
solon.config.load in YAML |
Declarative import |
@ConfigurationProperties(prefix="x") |
@BindProps(prefix="x") |
Prefix binding |
@Value("${x}") |
@Inject("${x}") |
Value injection |
The mental model is nearly identical. The key difference: Solon keeps environment logic in config (via solon.env.on segments or separate files) rather than scattering @Profile annotations across bean definitions.
Key Takeaways
-
solon.envis the single activation key — set it in the file, at the JVM, or as a container env var -
app-{env}.ymlis loaded automatically; no registration step required - Six loading tiers let you layer static defaults with runtime overrides cleanly
-
solon.config.loadwith${solon.env}handles per-module, per-environment config splitting - In-file
---segments withsolon.env.on(v2.5.5+) are unique to Solon — useful for small services - Config injection uses
@Inject("${...}")and@BindProps— both standard Solon IoC
The loading order is deterministic and documented. Deploy with confidence.
Top comments (0)