A Spring Boot library for managing application settings with multi-level storage support. Inspired by the Spring Data pattern, it provides a unified interface for accessing settings from memory, Redis, and MongoDB with automatic fallback capabilities. An optional admin layer exposes a secure REST API for reading and mutating settings at runtime without redeployment.
- 🚀 Multi-level Storage: Memory (Caffeine) → Redis → MongoDB fallback chain
- ⚡ Reactive Programming: Built on Project Reactor for non-blocking operations
- 🔧 Auto-configuration: Spring Boot auto-configuration for all modules
- 🎯 Composite Pattern: Chain multiple repositories for hierarchical storage
- 📦 Modular Design: Use only the storage backends you need
- 🛡️ Type Safety: Kotlin-first with full type safety
- ✅ Bean Validation: Optional Jakarta Bean Validation support for settings
- 🔑 Admin REST API: Optional HTTP layer for managing settings at runtime
- 🔐 Pluggable Auth: Keycloak JWT, HTTP Basic, or NOOP auth for the admin surface
- 📋 Audit Log: Optional change history with before/after values, caller identity, and one-click revert
dependencies {
// Core library (required)
implementation("io.github.mahdibohloul:spring-setting-core:1.0.0")
// Choose your storage backends
implementation("io.github.mahdibohloul:spring-setting-memory:1.0.0")
implementation("io.github.mahdibohloul:spring-setting-redis:1.0.0")
implementation("io.github.mahdibohloul:spring-setting-mongodb:1.0.0")
// Optional: headless admin facade (list / read / patch / replace / delete)
implementation("io.github.mahdibohloul:spring-setting-admin:1.0.0")
// Optional: expose the admin facade over HTTP (requires spring-boot-starter-webflux)
implementation("io.github.mahdibohloul:spring-setting-admin-webflux:1.0.0")
// Optional: back the HTTP admin layer with Keycloak JWT auth
implementation("io.github.mahdibohloul:spring-setting-admin-keycloak:1.0.0")
// Optional: enable Bean Validation for settings
implementation("org.springframework.boot:spring-boot-starter-validation")
}Note:
spring-setting-admin-webfluxandspring-setting-admin-keycloakdeclare WebFlux and Spring Security ascompileOnly— they will not force those stacks onto applications that do not need them. The consuming application must providespring-boot-starter-webfluxandspring-boot-starter-securityon its own classpath for the admin endpoints to activate.
data class DatabaseConfig(
val host: String,
val port: Int,
val database: String,
) : Setting@Service
class MyService(
private val settingService: SettingService,
) {
fun getDatabaseConfig(): Mono<DatabaseConfig> =
settingService.findByName("database.config", DatabaseConfig::class)
fun saveDatabaseConfig(config: DatabaseConfig): Mono<Void> =
settingService.save("database.config", config)
}When spring-boot-starter-validation is on the classpath the library automatically validates settings using
Jakarta Bean Validation. Validation runs before save (to reject invalid data) and on load (to fail fast on
corrupted or legacy data).
data class PaymentSetting(
@DecimalMax(value = "1000000", message = "Amount must be less than or equal to 1,000,000")
val maxAmount: BigDecimal,
@Future(message = "Expiry date must be in the future")
val expiryDate: LocalDate,
) : SettingIf no Validator bean is present validation is silently skipped.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Memory Cache │───▶│ Redis │───▶│ MongoDB │
│ (Caffeine) │ │ (Fast Cache) │ │ (Persistent) │
│ Fastest │ │ Distributed │ │ Reliable │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Consumer Application
│
▼
spring-setting-admin ← headless facade: list / read / patch / replace / delete / history / revert
│ audit SPI (SettingAuditLog, SettingAuditPrincipalProvider)
▼
spring-setting-mongodb ← optional: MongoDB-backed audit log + TransactionalOperator
│
▼
spring-setting-admin-webflux ← HTTP endpoints + Spring Security + per-type ACL
│ history & revert endpoints + reactive principal provider
▼
spring-setting-admin-keycloak ← optional: Keycloak JWT decoder + realm-role converter
interface SettingRepository {
fun <T : Setting> findByName(key: String, type: KClass<T>): Mono<T>
fun <T : Setting> deleteByName(key: String, type: KClass<T>): Mono<Void>
fun <T : Setting> save(key: String, setting: T): Mono<Void>
}Provides the base interfaces, composite pattern implementation, and optional Bean Validation integration.
reactor-core is an api dependency — consuming modules receive Reactor types on their compile classpath
without redeclaring it. slf4j-api is compileOnly; logging is provided by whatever SLF4J binding the host
application supplies.
Caffeine-based in-memory cache with configurable TTL.
spring:
setting:
memory:
maximum-size: 10000 # default
expire-after-write: PT1M # defaultReactive Redis implementation for distributed caching.
Configure the connection via Spring Boot's standard properties, then tune the library's own behaviour:
# Spring Boot connection config (required)
spring:
data:
redis:
host: localhost
port: 6379
# Library-specific properties
spring:
setting:
redis:
prefix: "setting:" # key prefix — default: "setting:"
ttl: PT5M # key TTL — default: PT5MReactive MongoDB implementation for persistent storage.
Configure the connection via Spring Boot's standard properties, then tune the library's own behaviour:
# Spring Boot connection config (required)
spring:
data:
mongodb:
uri: mongodb://localhost:27017/myapp
# Library-specific properties
spring:
setting:
mongodb:
collection-name: settings # collection used to store settings — default: "settings"Transport-agnostic façade for managing settings at runtime: list types, read current values, patch with JSON Merge Patch (RFC 7396), replace, and delete — with no HTTP or security assumptions.
Key SPIs:
SettingTypeDescriptor— register one bean perSettingtype to opt it into the admin API.SettingAdminService— headless service returning JSON strings.SettingAdminAuthorizer— replace the allow-all default with your own auth-context-aware implementation.
Usage:
// Describe your setting (recommended: same package as the Setting class).
@Component
class DelaySignalPolicySettingDescriptor : SettingTypeDescriptor<DelaySignalPolicySetting> {
override val settingClass = DelaySignalPolicySetting::class
override fun default() = DelaySignalPolicySetting()
}Setting persistence keys are derived via SettingHelper.getSettingName(...), so values written through the
admin API are immediately visible to SettingService.loadSetting(...) and @InjectSetting consumers.
Spring WebFlux REST + Spring Security layer on top of spring-setting-admin. Only activates in reactive
(WebApplicationType.REACTIVE) applications and only when spring.setting.admin.web.enabled=true is set.
Both WebFlux and Spring Security must be provided by the consuming application.
Endpoints (base path configurable via spring.setting.admin.web.base-path, default /spring-setting/admin):
| Method | Path | Operation |
|---|---|---|
GET |
{basePath}/settings |
List all registered type names |
GET |
{basePath}/settings/{type} |
Read current value (or default) |
PATCH |
{basePath}/settings/{type} |
RFC 7396 JSON Merge Patch |
PUT |
{basePath}/settings/{type} |
Full replace |
DELETE |
{basePath}/settings/{type} |
Reset to default |
GET |
{basePath}/settings/{type}/history |
Audit history (newest-first, ?limit=20) |
POST |
{basePath}/settings/{type}/revert/{id} |
Revert to the previousValue of that audit entry |
GET |
{basePath}/features |
Feature catalogue for UI consumers |
GET |
{basePath}/me |
Current caller's principal and roles |
Audit endpoints are only registered when
spring.setting.audit.enabled=true. Without that flag the routes are entirely absent.
Auth modes (spring.setting.admin.web.auth.mode):
| Mode | Description |
|---|---|
keycloak (default) |
OAuth2 Resource Server — validates JWT Bearer tokens using a ReactiveJwtDecoder bean. Provided by spring-setting-admin-keycloak or any custom ReactiveJwtDecoder bean. Returns 401 for missing/invalid tokens — does not redirect to a login page. |
basic |
HTTP Basic against a hardcoded user. Non-production only. |
noop |
No authentication. A synthetic Authentication carrying configured roles is injected so the per-type ACL still runs. Intended for integration tests and local dev. |
Resource Server vs OAuth2 Client: The admin layer is an OAuth2 Resource Server. It validates Bearer tokens — it never redirects a browser to a login page. To call the endpoints, obtain an access token from your authorization server first (e.g. via Postman,
curl, or your SPA's auth flow) and pass it asAuthorization: Bearer <token>.
Per-type per-operation ACL:
spring:
setting:
admin:
acl:
global:
list: [ ops-tribe, ops-manager, tech-manager ]
defaults:
read: [ ops-tribe, ops-manager, tech-manager ]
patch: [ ops-manager, tech-manager ]
replace: [ ops-manager, tech-manager ]
delete: [ tech-manager ]
history: [ ops-tribe, ops-manager, tech-manager ] # who may view change history
revert: [ ops-manager, tech-manager ] # who may revert to a previous value
profiles:
# Narrow the default for a specific setting type (key = simple class name).
DelayWeeklyEnforcementPolicySetting:
patch: [ tech-manager ]
delete: [ tech-manager ]
revert: [ tech-manager ]CORS:
spring:
setting:
admin:
web:
cors:
allowed-origins: [ "https://admin.example.com" ]
allowed-methods: [ GET, POST, PUT, PATCH, DELETE, OPTIONS ]
allowed-headers: [ Authorization, Content-Type ]
max-age: PT1HFeature SPI — register additional AdminFeatureDescriptor beans to expose extra admin operations in a
dynamic UI without changing the library.
UserDetailsServiceAutoConfiguration warning: @EnableWebFluxSecurity causes Spring Security's shared
AuthenticationConfiguration to register an ObjectPostProcessor bean, which Spring Boot's servlet-security
auto-configuration interprets as a signal to create an in-memory user with a random password. This module
registers a no-op UserDetailsService bean to suppress that warning automatically — no exclusion is needed
in the consuming application.
The audit log is an opt-in feature that records every PATCH, PUT, and DELETE operation with:
- the previous and new JSON value
- the caller identity (resolved from Spring Security context in the WebFlux layer)
- a timestamp
Add spring.setting.audit.enabled=true and include spring-setting-mongodb on the classpath to activate
MongoDB-backed persistence:
spring:
setting:
audit:
enabled: true
mongodb:
collection-name: setting_audit_logs # default
max-entries-per-type: 50 # default; 0 or negative disables evictionWithout spring.setting.audit.enabled=true a NoopSettingAuditLog is used and the history/revert
endpoints are not registered — existing behaviour is completely unchanged.
- After a setting is successfully saved (or deleted), an
AuditEntryis written with the before and after values. - Retention — after each write the oldest entries beyond
max-entries-per-typeare evicted. Eviction failures are logged and swallowed; they never affect the setting save. - Transactionality — when
spring-setting-mongodbdetects aReactiveMongoTransactionManagerbean (i.e. a MongoDB replica set with transaction management configured), the setting-save and audit-write are wrapped in a single atomic transaction. Both commit or both roll back. Without a transaction manager the audit write is best-effort.
POST {basePath}/settings/{type}/revert/{entryId} reads the previousValue from the named audit
entry and calls replace() with it. The revert itself is also audited as a REPLACE, giving a full
chain of custody in the history.
The library ships a ReactiveSecurityAuditPrincipalProvider that reads the authenticated username
from ReactiveSecurityContextHolder. To customise how the caller identity is resolved, register
your own SettingAuditPrincipalProvider bean:
@Bean
fun myPrincipalProvider(): SettingAuditPrincipalProvider =
SettingAuditPrincipalProvider {
ReactiveSecurityContextHolder.getContext()
.map { ctx -> ctx.authentication?.name ?: "system" }
}To store audit entries in a different backend, implement SettingAuditLog and register it as a bean:
@Bean
fun customAuditLog(): SettingAuditLog = MyCustomAuditLog()The MongoDB implementation (MongoSettingAuditLog) is skipped when a SettingAuditLog bean is
already present (@ConditionalOnMissingBean).
Wires Keycloak as the JWT issuer for spring-setting-admin-webflux. Provides a NimbusReactiveJwtDecoder
pointed at the realm's JWKS endpoint and a JWT authentication converter that extracts roles from two
sources in every token and merges them:
| Source | JWT claim path | When used |
|---|---|---|
| Realm roles | realm_access.roles |
Always |
| Client roles | resource_access.{client-id}.roles |
Only when client-id is set |
Both lists are merged into a single authority collection — no role is lost. If authority-prefix is
empty (the default), authority names match the role names exactly so the ACL config can use the same
strings without any transformation.
Configuration:
spring:
setting:
admin:
keycloak:
enabled: true
issuer-uri: https://auth.example.com/realms/my-realm # required — no default
authority-prefix: "" # leave empty to match ACL role names 1:1 (recommended)
client-id: "" # set to include resource_access.{clientId}.roles alongside realm rolesissuer-uri has no default and must be set explicitly. The JWKS endpoint is derived automatically as
{issuerUri}/protocol/openid-connect/certs.
Realm roles vs client roles: Keycloak lets you assign roles at the realm level (
realm_access) or scope them to a specific client (resource_access.<client-id>). Leaveclient-idempty to read only realm roles. Set it to your client's ID (e.g.delivery-admin-panel) when your users carry their meaningful roles as client-scoped roles — both sources are always merged.
Verify your Keycloak configuration before enabling:
# Confirm the realm is reachable and inspect supported grant types / token format.
curl -s https://auth.example.com/realms/my-realm/.well-known/openid-configuration | jq .
# Confirm the JWKS endpoint returns public keys.
curl -s https://auth.example.com/realms/my-realm/protocol/openid-connect/certs | jq .By default Keycloak issues signed JWT access tokens (RS256). If your realm is configured to issue opaque
(reference) tokens you will need a custom ReactiveJwtDecoder backed by token introspection instead.
spring:
setting:
create-default-instance: true # create default instance when none existsspring:
setting:
memory:
maximum-size: 10000 # max entries in cache
expire-after-write: PT1M # TTL after writespring:
setting:
redis:
prefix: "setting:" # key prefix
ttl: PT5M # key TTLspring:
setting:
mongodb:
collection-name: settings # collection used to store settingsspring:
setting:
audit:
enabled: false # opt-in; false by default
mongodb:
collection-name: setting_audit_logs # MongoDB collection for audit entries
max-entries-per-type: 50 # retention cap per setting type; ≤0 disables evictionspring:
setting:
admin:
web:
enabled: true
base-path: /spring-setting/admin
auth:
mode: keycloak # keycloak | basic | noop
basic: # used only when mode=basic
username: admin
password: admin
roles: [ ops-tribe, ops-manager, tech-manager ]
noop: # used only when mode=noop
roles: [ ops-tribe, ops-manager, tech-manager ]
synthetic-principal: dev-noop
cors:
allowed-origins: [ ]
allowed-methods: [ GET, POST, PUT, PATCH, DELETE, OPTIONS ]
allowed-headers: [ Authorization, Content-Type ]
exposed-headers: [ ]
allow-credentials: false
max-age: PT1H
acl:
global:
list: [ ]
defaults:
read: [ ]
patch: [ ]
replace: [ ]
delete: [ ]
history: [ ]
revert: [ ]
profiles: { }
keycloak:
enabled: false
issuer-uri: ""
authority-prefix: ""
client-id: "" # set to include resource_access.{clientId}.rolesThe library auto-configures individual repository beans but leaves the composite wiring to the consuming application for full transparency and control.
@Configuration
class SettingConfiguration {
// Three-tier: Memory → Redis → MongoDB
@Bean
@Primary
fun compositeSettingRepository(
memoryRepo: SettingRepository,
redisRepo: SettingRepository,
mongoRepo: SettingRepository,
): SettingRepository = CompositeSettingRepository(
listOf(memoryRepo, redisRepo, mongoRepo)
)
}Other common combinations:
// Memory + MongoDB (no Redis)
CompositeSettingRepository(listOf(memoryRepo, mongoRepo))
// Memory only (dev / tests)
CompositeSettingRepository(listOf(memoryRepo))git clone https://github.com/mahdibohloul/spring-setting.git
cd spring-setting
./gradlew build- Fork the repository
- Create a feature branch
- Make your changes with tests
- Submit a pull request
This project is licensed under the MIT License — see the LICENSE file for details.
- Inspired by Spring Data patterns
- Built with Project Reactor for reactive programming
- Uses Caffeine for high-performance caching