Ravel resolves the MicroProfile Config abstraction against the JDK and the spec alone — zero third-party libraries, strict JPMS, jlink-ready. This page documents the module structure and the design choices behind the runtime.
Overview
Ravel implements MicroProfile Config 3.1 with these principles:
-
Zero external dependencies — only
org.eclipse.microprofile.config -
JPMS strict — all modules have explicit
module-info, no automatic modules -
jlink-ready — can be included in custom JDK images via
jlink -
Virtual thread–friendly — no
synchronizedorThreadLocal, usesScopedValuefor context
Module structure
ravel-mp-config-api
Intermediate repackaging layer. The original microprofile-config-api JAR only has an Automatic-Module-Name manifest entry, not a proper module-info.class. This module:
-
Unpacks the original JAR
-
Compiles a new
module-info.javawith the same module name (org.eclipse.microprofile.config) -
Produces a JAR that
jlinkcan consume directly
All other Ravel modules depend on this repackaged JAR, not directly on the MicroProfile spec.
ravel-api
Public API re-export. Exposes the MicroProfile Config spec interfaces:
-
ConfigProvider— entry point -
Config— configuration access interface -
ConfigSource,ConfigSourceProvider— SPI for custom sources -
Converter— type conversion SPI -
ConfigBuilder— programmatic configuration
Also declares:
uses org.eclipse.microprofile.config.spi.ConfigSource;
uses org.eclipse.microprofile.config.spi.ConfigSourceProvider;
uses org.eclipse.microprofile.config.spi.Converter;
ravel-core
Implementation core. Contains all business logic:
-
RavelConfigProviderResolver—ServiceLoader-based resolver forConfigProviderResolver -
RavelConfigBuilder— builder implementation -
RavelConfig— main config object (immutable, thread-safe) -
Built-in
ConfigSourceimplementations — system properties, environment variables,microprofile-config.properties -
Converterregistry — built-in converters for primitives, collections, temporal types, etc. -
ExpressionResolver— property expression evaluation with cycle detection -
ProfiledConfigSource— profile-aware wrapping of sources
No external dependencies. Can be used standalone without CDI.
ravel-cdi-vauban
CDI integration. Provides:
-
@ConfigPropertyannotation processing — via Vauban Build-Compatible Extension -
Injection of configuration values — including
Optional<T>support -
Deployment-time validation — thrown as
DeploymentExceptionif config is missing
Depends on ravel-core + jakarta.cdi. Optional; if not on classpath, CDI injection simply won’t work, but ravel-core continues to function.
Lookup flow
ConfigProvider.getConfig()
└─> RavelConfigProviderResolver (ServiceLoader)
└─> RavelConfigBuilder.build()
└─> RavelConfig constructor
├─ Load ConfigSources (ServiceLoader + built-in)
├─ Sort by ordinal (descending)
└─ Lazy-initialize derived converters cache
config.getValue("key", String.class)
└─> RavelConfig.getValue()
├─ Cascade through ConfigSources by ordinal
│ ├─ SystemPropertiesConfigSource (ordinal 400)
│ ├─ EnvironmentVariablesConfigSource (ordinal 300)
│ ├─ MicroprofilePropertiesConfigSource (ordinal 100)
│ └─ Custom ConfigSources (SPI)
├─ Resolve property expressions (${key}, ${key:default})
│ └─ Detect cycles via ScopedValue<Set<String>>
├─ Match config profiles (%dev., %prod., %test.)
├─ Find matching Converter<String> (priority 1: built-in)
└─ Return converted value or throw NoSuchElementException
Property expression resolution
The raw value from a config source may contain expressions like ${app.name} or ${app.version:1.0}. These are resolved before type conversion:
-
Raw value:
${app.name} v${app.version:1.0} -
Parse expressions: find all
${…}patterns -
Recursively resolve each reference with cycle detection
-
Replace with resolved values
-
Result:
MyApp v2.0(then converted to target type)
Cycle detection uses ScopedValue<Set<String>> to track currently-resolving keys. If a key appears twice in the stack, IllegalArgumentException is thrown.
Configuration profile matching
Config profiles are activated via system property mp.config.profile:
java -Dmp.config.profile=dev MyApplication
When a profile is active:
-
ProfiledConfigSourcewraps each source with ordinal +1 -
Keys are matched as %{profile}.{original-key}
-
If not found, falls back to original key
-
Profiles are resolved at lookup time, not build time
Example:
app.debug=false
%dev.app.debug=true
%prod.app.port=8443
With mp.config.profile=dev:
- app.debug → first checks %dev.app.debug (found: true) → returns true
- app.port → checks %dev.app.port (not found), falls back to app.port (not found) → NoSuchElementException
With mp.config.profile=prod:
- app.port → checks %prod.app.port (found: 8443) → returns 8443
- app.debug → checks %prod.app.debug (not found), falls back to app.debug (found: false) → returns false
Type conversion
Ravel uses a three-tier converter lookup:
-
Built-in converters (priority 1) — primitives, collections, temporal types, standard library types
-
Implicit converters (priority 2) — types with public
Stringconstructor orvalueOf(String)/parse(CharSequence)methods -
Custom converters (priority > 2) — user-registered via
ConfigBuilder.withConverter()or ServiceLoader
If no converter matches, IllegalArgumentException is thrown.
Thread safety
-
RavelConfigis immutable and thread-safe — can be safely shared across threads -
ConfigProvider.getConfig()returns a cached instance — sameConfigobject on repeated calls -
No
synchronizedblocks orThreadLocal— usesScopedValuefor context (e.g., expression resolution stack) -
ConcurrentHashMapfor derived converters cache — safe concurrent access to lazy-initialized converters
Performance considerations
-
Lazy initialization — converters are not instantiated until first use
-
SPI discovery via
ServiceLoader— once at startup, then cached -
Configuration sources are ordered by ordinal — lookup short-circuits on first match
-
Array and collection conversion — split and convert on demand, cached in
RavelConfig.derivedConverters
Next
-
Config Sources — how to implement custom sources
-
Type Converters — how to implement custom converters