Configuration and Precedence
Configuration Profiles Properties and Precedence
Konfigurasi service enterprise lintas environment: source, profile, precedence, validation, runtime reload, drift, dan safe defaults
Part 039 — Configuration, Profiles, Properties, and Precedence
Fokus part ini: memahami configuration sebagai runtime contract. Di production, banyak incident bukan karena algoritma salah, tetapi karena konfigurasi salah, precedence tidak jelas, default terlalu permissive, config drift, atau secret/config tertukar.
Dalam service enterprise, konfigurasi bukan sekadar file .properties.
Konfigurasi menentukan:
- service berjalan di environment mana
- dependency mana yang dipanggil
- timeout berapa
- retry aktif atau tidak
- database schema mana yang dipakai
- Kafka topic apa yang dipublish/consume
- feature mana yang enable
- tenant mana memakai rule/catalog tertentu
- observability dikirim ke mana
- security policy apa yang enforce
Senior engineer harus bisa menjawab:
For a given running pod/process, what exact configuration is active, from which source, with what precedence, and how can we prove it safely?
1. Configuration Is Runtime Behavior
Code biasanya sama di semua environment.
Behavior berbeda karena config.
same artifact + different config = different system behavior
Contoh:
quote.order.timeout.ms=3000
quote.order.retry.maxAttempts=2
quote.order.kafka.topic=quote-order-events-v2
quote.order.db.pool.maxSize=30
Empat property ini dapat mengubah:
- latency
- throughput
- retry amplification
- event compatibility
- database pressure
- incident blast radius
Karena itu config harus diperlakukan sebagai bagian dari architecture.
Bukan detail deployment semata.
2. Configuration vs Secret vs Runtime State
Jangan campur tiga hal ini.
| Category | Example | Should be stored as | Main risk |
|---|---|---|---|
| Configuration | timeout, endpoint URL, pool size | config source | wrong behavior |
| Secret | password, private key, token | secret manager | leak/compromise |
| Runtime state | current offset, last processed ID | database/cache/Kafka | data corruption |
Bad smell:
db.password=plain-text-password
lastProcessedOrderId=ORD-123
Yang pertama adalah secret. Yang kedua adalah state. Keduanya tidak seharusnya menjadi normal config property.
Senior rule:
Configuration should describe desired behavior, not hold confidential material or mutable business state.
3. Common Configuration Sources
Dalam Java/JAX-RS/Jersey/Kubernetes service, config bisa datang dari banyak tempat.
Source examples:
- hardcoded default
- Java system property: -Dkey=value
- environment variable
- application.properties
- YAML file
- profile-specific property file
- Kubernetes ConfigMap
- Kubernetes Secret
- mounted file
- Helm values
- GitOps manifest
- cloud parameter store
- cloud app configuration service
- feature flag platform
- database-backed tenant configuration
Masalahnya bukan banyak source. Masalahnya adalah jika precedence tidak jelas.
4. Configuration Precedence
Configuration precedence adalah aturan siapa menang ketika property yang sama muncul di banyak source.
Contoh property:
quote.http.client.timeoutMs
Muncul di:
- default code value: 1000
- application.properties: 2000
- application-prod.properties: 3000
- environment variable: 5000
- Kubernetes ConfigMap: 4000
- command line system property: 7000
Pertanyaannya:
value final yang dipakai berapa?
Tanpa precedence yang eksplisit, debugging menjadi tebak-tebakan.
Contoh precedence yang umum, bukan aturan universal:
1. hardcoded safe default
2. base properties file
3. profile-specific properties file
4. environment variables / mounted files
5. system properties / startup override
6. emergency runtime override / feature flag platform
Tetapi jangan menganggap urutan ini benar di codebase internal. Harus diverifikasi.
5. Profile Is Not Tenant
Profile menjawab:
service berjalan di environment atau mode apa?
Tenant menjawab:
request/data/config ini milik tenant siapa?
Jangan mencampur keduanya.
Bad pattern:
profile=prod-customerA
profile=prod-customerB
Ini membuat environment dan tenancy bercampur. Lebih sehat:
profile=prod
region=ap-southeast-1
tenant=resolved per request or per job context
Dalam CPQ/order management system, tenant-specific catalog/pricing/rules mungkin ada. Tetapi itu bukan berarti application profile harus per tenant.
6. Environment Profile Model
Profile umum:
local
unit-test
integration-test
dev
qa
staging
preprod
prod
Tetapi real enterprise sering punya dimensi tambahan:
region
cloud provider
customer segment
deployment mode
on-prem vs cloud
regulated vs non-regulated environment
Hindari profile explosion:
prod-aws-us-east-customerA-onprem-like-special-case
Gunakan dimensi eksplisit:
app.profile=prod
app.region=us-east-1
app.cloudProvider=aws
app.deploymentMode=cloud
Atau:
app:
profile: prod
region: us-east-1
cloudProvider: aws
deploymentMode: cloud
7. Build-Time vs Deploy-Time vs Runtime Configuration
Pemisahan penting:
| Timing | Example | Change requires rebuild? | Risk |
|---|---|---|---|
| Build-time | Java version, dependency, generated code | yes | artifact mismatch |
| Deploy-time | env var, ConfigMap, JVM flag | no rebuild, yes redeploy/restart | rollout drift |
| Runtime dynamic | feature flag, dynamic config | no | inconsistent behavior |
| Request-time | tenant config, identity, locale | no | cross-request leak |
Senior review harus bertanya:
If this value changes, do we need rebuild, redeploy, restart, or just dynamic propagation?
8. Jersey/JAX-RS Configuration Layers
Ada beberapa level config yang mudah tercampur.
8.1 JAX-RS Application Configuration
Standar JAX-RS punya Application sebagai application boundary.
@ApplicationPath("/api")
public class QuoteApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
return Set.of(QuoteResource.class);
}
}
Ini lebih tentang resource/provider registration. Bukan full enterprise config system.
8.2 Jersey ResourceConfig Properties
Jersey punya ResourceConfig.
public class QuoteResourceConfig extends ResourceConfig {
public QuoteResourceConfig() {
packages("com.example.quote.api");
property("jersey.config.server.tracing.type", "OFF");
}
}
property(...) di sini adalah Jersey-specific configuration.
Jangan otomatis disamakan dengan application business config.
8.3 Application Business Configuration
Contoh business/runtime config:
public record QuoteServiceConfig(
Duration quoteCalculationTimeout,
int maxLineItems,
boolean enableExperimentalPricing,
String kafkaTopic
) {}
Ini sebaiknya di-load, divalidasi, dan di-inject sebagai typed object.
9. Typed Configuration Object
Config yang tersebar sebagai string raw membuat system rapuh.
Bad pattern:
String timeout = System.getenv("QUOTE_TIMEOUT_MS");
client.setTimeout(Integer.parseInt(timeout));
Lebih baik:
public record HttpClientConfig(
Duration connectTimeout,
Duration readTimeout,
int maxConnections,
boolean retryEnabled
) {
public HttpClientConfig {
if (connectTimeout == null || connectTimeout.isNegative() || connectTimeout.isZero()) {
throw new IllegalArgumentException("connectTimeout must be positive");
}
if (readTimeout == null || readTimeout.isNegative() || readTimeout.isZero()) {
throw new IllegalArgumentException("readTimeout must be positive");
}
if (maxConnections <= 0) {
throw new IllegalArgumentException("maxConnections must be positive");
}
}
}
Benefits:
- config has type
- validation is centralized
- startup can fail fast
- tests can construct explicit config
- PR review becomes easier
10. Fail Fast on Invalid Configuration
Configuration error should be detected at startup whenever possible.
Bad behavior:
service starts successfully
first production request reaches code path
missing config causes 500
Better behavior:
service refuses to start
deployment fails early
bad config never receives production traffic
Example validator:
public final class ConfigValidator {
public static void validate(QuoteServiceConfig config) {
requirePositive(config.maxLineItems(), "maxLineItems");
requirePresent(config.kafkaTopic(), "kafkaTopic");
requirePositiveDuration(config.quoteCalculationTimeout(), "quoteCalculationTimeout");
}
private static void requirePresent(String value, String name) {
if (value == null || value.isBlank()) {
throw new IllegalStateException("Missing required config: " + name);
}
}
private static void requirePositive(int value, String name) {
if (value <= 0) {
throw new IllegalStateException("Invalid config: " + name + " must be positive");
}
}
private static void requirePositiveDuration(Duration value, String name) {
if (value == null || value.isZero() || value.isNegative()) {
throw new IllegalStateException("Invalid config: " + name + " must be positive");
}
}
}
Senior rule:
A service should not become ready if required configuration is invalid.
11. Safe Defaults
Default is dangerous if it hides missing config.
Example unsafe default:
boolean authEnabled = getBoolean("auth.enabled", false);
In production, missing config disables auth.
Better:
boolean authEnabled = requireBoolean("auth.enabled");
Or fail closed:
boolean authEnabled = getBoolean("auth.enabled", true);
But even true default may hide intent.
For sensitive controls, prefer explicit config.
Safe default principle:
Default should reduce blast radius, not maximize convenience.
Examples:
| Config | Safer default |
|---|---|
| auth enabled | true or required |
| debug endpoint | false |
| retry max attempts | low or disabled until explicit |
| max page size | bounded |
| file upload size | small bounded default |
| cache TTL | short or required |
| allow cross-origin | false |
| tenant fallback | no fallback |
12. Configuration Naming
Good config names are stable contracts.
Bad:
timeout=5000
url=http://service
flag=true
Better:
quote.pricing.http.connectTimeoutMs=500
quote.pricing.http.readTimeoutMs=3000
quote.pricing.http.baseUrl=https://pricing.internal
quote.pricing.feature.enableNewDiscountEngine=false
Naming guidelines:
- namespace by domain/component
- include dependency name
- include unit in name or use typed duration format
- avoid vague names
- avoid overloaded generic keys
- distinguish connect/read/request/idle timeout
13. Units Must Be Explicit
This is a real production failure source.
Bad:
quote.timeout=5
What is 5?
5 ms?
5 seconds?
5 minutes?
Better:
quote.timeoutMs=5000
Or typed duration:
quote.timeout=5s
But typed duration requires a parser and convention.
Senior rule:
If a config value has a unit, the unit must be unambiguous at the declaration site.
14. Runtime Reload
Runtime reload means config can change without restarting service.
This is powerful and dangerous.
Good use cases:
- feature flag
- emergency kill switch
- traffic percentage
- non-critical threshold
- temporary routing override
Risky use cases:
- DB connection URL
- schema compatibility behavior
- security policy
- object mapper behavior
- Kafka topic name
- tenant isolation mode
Runtime reload hazards:
- half of instances use old value
- in-flight request sees inconsistent values
- dependency clients are not rebuilt safely
- caches are not invalidated
- observability does not show config version
- rollback is unclear
If dynamic config exists, it needs:
- version
- audit trail
- owner
- validation
- propagation model
- rollback model
- observability
15. Immutable Config Snapshot Per Request
When config can change at runtime, avoid reading global config repeatedly during one operation.
Bad:
public QuoteResponse calculate(QuoteRequest request) {
if (config.flags().newPricing()) {
// path A
}
Money price = pricingEngine.calculate(request, config.pricingRules());
if (config.flags().newPricing()) {
// path B
}
}
If config reloads between reads, path A and path B may disagree.
Better:
public QuoteResponse calculate(QuoteRequest request) {
QuoteRuntimeConfig snapshot = configProvider.currentSnapshot();
if (snapshot.flags().newPricing()) {
// path A
}
Money price = pricingEngine.calculate(request, snapshot.pricingRules());
if (snapshot.flags().newPricing()) {
// path B
}
}
Rule:
Use a stable config snapshot for one business operation.
16. Config Version and Observability
Dynamic config should have identity.
Expose safely:
- app version
- config version/hash
- active profile
- region
- feature flag version
- schema version
Do not expose:
- secret values
- credentials
- tenant sensitive settings
- customer-specific pricing rules unless authorized
Possible safe health metadata:
{
"appVersion": "1.42.3",
"profile": "prod",
"region": "ap-southeast-1",
"configVersion": "2026-07-10T08:30:00Z-7f3a",
"runtime": "jaxrs-jersey",
"status": "UP"
}
Internal verification:
Can operators identify which config version a failing pod used?
If not, incident debugging becomes expensive.
17. Configuration Drift
Configuration drift means environments or instances that should be equivalent are no longer equivalent.
Examples:
- pod A has timeout 3s, pod B has timeout 10s
- staging uses Kafka topic v1, prod uses v2
- one region has feature enabled, another not
- local default differs from deployment default
- database migration assumes config that only some pods have
Drift causes non-deterministic bugs.
Detection techniques:
- GitOps desired state
- config hash in logs/metrics
- startup config summary
- environment diff tool
- deployment manifest review
- config conformance test
18. Config and Kubernetes
In Kubernetes, config usually appears as:
- environment variables
- ConfigMap mounted as env
- ConfigMap mounted as file
- Secret mounted as env
- Secret mounted as file
- projected volume
- external secrets operator
- Helm values rendered to manifest
Key difference:
Env var changes usually require pod restart.
Mounted files may update, but application may not reload them safely.
Do not assume changing ConfigMap updates running behavior. Verify:
- does pod restart on config change?
- is checksum annotation used?
- does app watch mounted file?
- is reload atomic?
- what happens to in-flight requests?
19. Config and Readiness
A pod should not be ready if required config/dependency config is invalid.
Readiness should fail for:
- missing required config
- invalid config value
- malformed URL
- unsupported profile
- invalid tenant config baseline
- incompatible feature flag state
But readiness should not perform heavy operations forever.
Balance:
startup validation = deep config validation
readiness = current ability to serve
liveness = process health, not dependency health
20. Config for HTTP Client Resilience
Outbound client config should not be vague.
Bad:
pricing.timeout=3000
pricing.retry=true
Better:
pricing.http.connectTimeoutMs=500
pricing.http.readTimeoutMs=3000
pricing.http.maxConnections=100
pricing.resilience.retry.maxAttempts=2
pricing.resilience.retry.backoffMs=100
pricing.resilience.circuitBreaker.failureRateThreshold=50
pricing.resilience.circuitBreaker.openStateWaitMs=30000
Review questions:
- is timeout shorter than caller timeout?
- is retry budget bounded?
- does retry only happen for safe/idempotent operations?
- does circuit breaker protect the downstream?
- is fallback safe?
21. Config for Database
Database config must be reviewed with workload in mind.
Examples:
db.pool.maxSize=30
db.pool.minIdle=5
db.pool.connectionTimeoutMs=1000
db.pool.idleTimeoutMs=600000
db.statement.timeoutMs=5000
db.transaction.defaultIsolation=READ_COMMITTED
Bad values can create incidents:
- pool too large overloads DB
- pool too small causes app queueing
- statement timeout too high holds locks too long
- isolation too strict reduces throughput
- missing transaction timeout causes stuck transactions
22. Config for Kafka
Kafka config affects correctness.
Examples:
quote.events.topic=quote-events-v2
quote.events.producer.acks=all
quote.events.producer.enableIdempotence=true
quote.events.consumer.groupId=quote-order-service
quote.events.consumer.maxPollRecords=100
quote.events.consumer.autoOffsetReset=earliest
Review questions:
- is topic compatible with schema version?
- is group ID stable?
- is max poll size aligned with processing time?
- is auto offset reset appropriate?
- is idempotency enabled where needed?
23. Config for Logging and Telemetry
Logging config must balance visibility and risk.
Examples:
logging.level.root=INFO
logging.level.com.example.quote=INFO
logging.redaction.enabled=true
telemetry.tracing.enabled=true
telemetry.sampling.ratio=0.1
metrics.highCardinalityLabels.disabled=true
Bad config:
- DEBUG enabled globally in production
- request/response body logging enabled with PII
- trace sampling too low during incident
- high-cardinality labels include quoteId/orderId/customerId
24. Configuration Anti-Patterns
24.1 Hardcoded Production URL
private static final String PRICING_URL = "https://pricing.prod.internal";
Problems:
- impossible to test safely
- breaks environment parity
- hidden dependency
24.2 Boolean Flag Without Owner
enableNewFlow=true
Questions:
- who owns it?
- when is it removed?
- is it tenant-specific?
- what is default?
- is rollback tested?
24.3 Silent Fallback
String topic = get("topic", "default-topic");
If topic is missing in production, service silently publishes to wrong topic.
24.4 Config Read Everywhere
System.getenv("...")
Scattered config reads make precedence and validation impossible.
24.5 Secrets in Config Dump
Operators need visibility, but not secret values.
Use redacted summaries.
25. Debugging Configuration Problems
When behavior differs across environments, debug config first.
Checklist:
1. identify running artifact version
2. identify active profile
3. identify active config version/hash
4. inspect startup config summary
5. compare desired manifest vs running pod env/mounts
6. compare pod A vs pod B
7. check feature flag state
8. check tenant-specific override
9. check cloud parameter/app config version
10. check last deployment/config change timeline
Useful commands in Kubernetes context:
kubectl describe pod <pod>
kubectl exec <pod> -- printenv | sort
kubectl get configmap <name> -o yaml
kubectl get secret <name> -o yaml # do not decode casually in shared logs
kubectl rollout history deployment <name>
Do not paste secret values into tickets or chat.
26. Example Configuration Loader Boundary
A simple pattern:
public interface ConfigSource {
Optional<String> get(String key);
}
public final class CompositeConfigSource implements ConfigSource {
private final List<ConfigSource> sourcesInPriorityOrder;
public CompositeConfigSource(List<ConfigSource> sourcesInPriorityOrder) {
this.sourcesInPriorityOrder = List.copyOf(sourcesInPriorityOrder);
}
@Override
public Optional<String> get(String key) {
for (ConfigSource source : sourcesInPriorityOrder) {
Optional<String> value = source.get(key);
if (value.isPresent()) {
return value;
}
}
return Optional.empty();
}
}
Then build typed config:
public final class QuoteConfigFactory {
private final ConfigSource source;
public QuoteConfigFactory(ConfigSource source) {
this.source = source;
}
public QuoteServiceConfig load() {
QuoteServiceConfig config = new QuoteServiceConfig(
requireDuration("quote.calculation.timeout"),
requireInt("quote.lineItems.max"),
requireBoolean("quote.pricing.experimental.enabled"),
requireString("quote.events.topic")
);
ConfigValidator.validate(config);
return config;
}
private String requireString(String key) {
return source.get(key)
.filter(value -> !value.isBlank())
.orElseThrow(() -> new IllegalStateException("Missing required config: " + key));
}
private int requireInt(String key) {
try {
return Integer.parseInt(requireString(key));
} catch (NumberFormatException e) {
throw new IllegalStateException("Invalid integer config: " + key, e);
}
}
private boolean requireBoolean(String key) {
String value = requireString(key);
if (value.equalsIgnoreCase("true")) return true;
if (value.equalsIgnoreCase("false")) return false;
throw new IllegalStateException("Invalid boolean config: " + key);
}
private Duration requireDuration(String key) {
return Duration.parse(requireString(key));
}
}
This is illustrative, not a mandate. In real enterprise systems, you may use MicroProfile Config, Spring-like config infrastructure, custom platform config, cloud config service, or internal libraries.
Internal verification matters more than guessing.
27. Configuration and PR Review
When reviewing PR that adds config, ask:
- what is the default?
- is the default safe?
- is the config required?
- what is the unit?
- where is it documented?
- who owns it?
- can it differ per environment?
- can it differ per tenant?
- does it require restart?
- is it secret?
- is it logged safely?
- is it validated at startup?
- what happens if it is missing?
- what happens if it is malformed?
- how is it rolled back?
A config PR is not trivial. It changes runtime behavior.
28. Failure Modes
| Failure mode | Symptom | Root cause | Detection |
|---|---|---|---|
| Missing required config | startup failure or 500 | property absent | startup validation |
| Wrong precedence | unexpected value active | source order unclear | config summary/hash |
| Unsafe default | security/performance incident | missing value hidden | config review |
| Drift between pods | intermittent behavior | inconsistent config | pod diff/config hash |
| Unit mismatch | timeout too high/low | ms vs sec confusion | typed config/unit naming |
| Secret leak | credential exposed | config dump/log | redaction scan |
| Runtime reload inconsistency | split behavior | partial propagation | config version metrics |
| Tenant config leak | wrong tenant behavior | fallback/lookup bug | tenant-aware audit |
29. Internal Verification Checklist
For CSG Quote & Order or any internal enterprise service, verify from evidence:
Repository and code:
- where configuration is loaded
- whether config is typed or raw string based
- whether MicroProfile Config, custom config, Jersey property, or another system is used
- whether `ResourceConfig.property(...)` is used for Jersey-specific behavior
- whether config validation runs at startup
- whether default values exist and where
- whether units are explicit
Runtime and deployment:
- active profile naming convention
- config source order and precedence
- Kubernetes ConfigMap/Secret usage
- Helm/GitOps values source
- whether config change triggers pod restart
- whether config hash is exposed in logs/metrics
- whether environment drift is detectable
Security:
- whether secrets are stored separately from config
- whether config dumps are redacted
- whether secret rotation is documented
- whether emergency overrides are audited
Operations:
- how operators inspect active config safely
- how config changes are reviewed
- how rollback is performed
- how config drift is detected
- how tenant-specific config is governed
Do not assume the internal stack. Prove it from code, deployment manifests, pipeline, docs, and team discussion.
30. Senior Mental Model
Think of configuration as a distributed control plane.
code defines possible behavior
configuration selects active behavior
runtime applies behavior
observability proves behavior
governance controls behavior changes
A senior engineer does not merely ask:
What value should I put here?
A senior engineer asks:
Who owns this value?
Where does it come from?
What wins if it conflicts?
How is it validated?
How is it observed?
How is it rolled back?
What is the failure mode if it is wrong?
31. Practical Exercises
- Pick one existing service and list all config sources.
- Trace one config key from repository to running pod.
- Identify the exact precedence order.
- Find one config that has no explicit unit.
- Find one config whose default may be unsafe.
- Find one config that should be secret but is not treated as secret.
- Find how active config version is visible during an incident.
- Check whether config drift between pods can be detected.
32. Part Summary
Configuration is not support material. It is runtime architecture.
You should now be able to reason about:
- config vs secret vs state
- source and precedence
- profile vs tenant
- build-time/deploy-time/runtime/request-time config
- typed configuration
- validation and fail-fast startup
- safe defaults
- runtime reload risk
- config versioning and observability
- drift detection
- Kubernetes config behavior
- PR review for config changes
Next part: multi-tenancy and enterprise configuration — where configuration becomes tenant-aware and directly affects catalog, pricing, routing, logging, and data isolation.
You just completed lesson 39 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.