Developer Guide
This section covers the data model, configuration reference, required integration points, and extension points available to developers integrating Configuration Manager into an application.
Data Model
Configuration Manager persists three core entities. A system configuration defines a global key-value setting managed by administrators. A user configuration defines a per-user key-value setting where each user can hold their own value. A configuration value is the shared value record used by both types, carrying the actual data in one of six typed columns.
Neither configuration entity stores a pointer to its current value. Each one holds a default value and a list of historical values, and the current value is the most recent entry in that list — falling back to the default when the list is empty. History is tracked through two join tables: cm_sc_history records the values of system configurations, and cm_uc_history records the per-user values of user configurations.
---
config:
layout: elk
---
erDiagram
cm_sc {
integer id PK
varchar name
varchar description
varchar value_type
timestamp created
integer default_value FK
}
cm_uc {
integer id PK
varchar name
varchar description
varchar value_type
timestamp created
integer default_value FK
}
cm_config_value {
integer id PK
varchar owner_username
varchar editor_username
varchar value_type
varchar string_value
integer int_value
date date_value
time time_value
boolean bool_value
numeric decimal_value
timestamp created
}
cm_sc_history {
integer config_id FK
integer value_id FK
}
cm_uc_history {
integer user_configuration_entity_id FK
integer user_values_id FK
}
cm_sc ||--o| cm_config_value : "default value"
cm_sc ||--o{ cm_sc_history : "values history"
cm_sc_history }o--|| cm_config_value : ""
cm_uc ||--o| cm_config_value : "default value"
cm_uc ||--o{ cm_uc_history : "user values"
cm_uc_history }o--|| cm_config_value : ""
Database Schema
Configuration Manager manages five tables. The schema below represents the DDL generated by Hibernate for a standard relational database.
CREATE SEQUENCE cm_sc_seq
START WITH 1 INCREMENT BY 50;
CREATE SEQUENCE cm_uc_seq
START WITH 1 INCREMENT BY 50;
CREATE SEQUENCE cm_config_value_seq
START WITH 1 INCREMENT BY 50;
CREATE TABLE cm_config_value (
id INTEGER NOT NULL,
owner_username VARCHAR(255),
editor_username VARCHAR(255),
value_type VARCHAR(255),
string_value VARCHAR(255),
int_value INTEGER,
date_value DATE,
time_value TIME,
bool_value BOOLEAN,
decimal_value NUMERIC(30, 20),
created TIMESTAMP(6),
CONSTRAINT pk_cm_config_value PRIMARY KEY (id)
);
CREATE TABLE cm_sc (
id INTEGER NOT NULL,
name VARCHAR(255) NOT NULL,
description VARCHAR(255),
value_type VARCHAR(255),
created TIMESTAMP(6),
default_value INTEGER,
CONSTRAINT pk_cm_sc PRIMARY KEY (id),
CONSTRAINT fk_cm_sc_default_value FOREIGN KEY (default_value) REFERENCES cm_config_value (id)
);
CREATE TABLE cm_uc (
id INTEGER NOT NULL,
name VARCHAR(255) NOT NULL,
description VARCHAR(255),
value_type VARCHAR(255),
created TIMESTAMP(6),
default_value INTEGER,
CONSTRAINT pk_cm_uc PRIMARY KEY (id),
CONSTRAINT fk_cm_uc_default_value FOREIGN KEY (default_value) REFERENCES cm_config_value (id)
);
CREATE TABLE cm_sc_history (
config_id INTEGER NOT NULL,
value_id INTEGER NOT NULL,
CONSTRAINT fk_cm_sc_history_sc FOREIGN KEY (config_id) REFERENCES cm_sc (id),
CONSTRAINT fk_cm_sc_history_value FOREIGN KEY (value_id) REFERENCES cm_config_value (id)
);
CREATE TABLE cm_uc_history (
user_configuration_entity_id INTEGER NOT NULL,
user_values_id INTEGER NOT NULL,
CONSTRAINT fk_cm_uc_history_uc FOREIGN KEY (user_configuration_entity_id) REFERENCES cm_uc (id),
CONSTRAINT fk_cm_uc_history_value FOREIGN KEY (user_values_id) REFERENCES cm_config_value (id)
);
cm_config_value is created first because both cm_sc and cm_uc hold foreign keys to it. The value_type column in each table stores the string representation of the ConfigurationType enum (INTEGER, STRING, BIG_DECIMAL, BOOLEAN, DATE, TIME). Only the column matching the declared type is populated for a given value record; the others remain null. The decimal_value column is defined with precision 30 and scale 20 to accommodate high-precision values.
Each value record also carries two usernames: owner_username identifies the user a value belongs to (null for system configurations), and editor_username identifies the user who saved it. The created column is mapped from a java.time.Instant, so timestamps are stored as an absolute instant and rendered in each user's own time zone.
Module Overview
Configuration Manager is structured as six Maven modules following the AppJars layered architecture:
| Module | Artifact ID | Description |
|---|---|---|
| Model | appjars-configuration-manager-model |
DTOs, ConfigurationType enum, auto-configuration |
| Business API | appjars-configuration-manager-business |
SystemConfigurationService, UserConfigurationService, ConfigurationValueService, ApplicationRestarter, UserProvider interfaces |
| Business Impl | appjars-configuration-manager-business-impl |
Service implementations and the application restart mechanism |
| Data API | appjars-configuration-manager-data |
SystemConfigurationDao, UserConfigurationDao, ConfigurationValueDao interfaces |
| Data Impl | appjars-configuration-manager-data-impl |
JPA entities, DAO implementations, converters, and the database-backed property source |
| Flow UI | appjars-configuration-manager-flow |
Three Vaadin views and route configuration |
A monolithic application includes the three implementation modules (-business-impl, -data-impl, -flow). The API modules (-business, -data) are pulled in transitively.
Spring Auto-Configuration
Configuration Manager registers itself through Spring Boot's auto-configuration mechanism. The entry point is ConfigurationManagerAutoConfiguration, declared in:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
This class scans the com.appjars.configurationmanager package for all Spring components and JPA entities.
Two further hooks are registered through META-INF/spring.factories, because both must run before the application context exists:
ConfigurationManagerEnvironmentPostProcessor(anEnvironmentPostProcessor, in-data-impl) installs the database-backed property source into the SpringEnvironment.SpringApplicationHolder(aSpringApplicationRunListener, in-business-impl) captures the runningSpringApplicationso that the context can be restarted later.
Configuration Properties
View URLs
| Property | Default | Description |
|---|---|---|
com.appjars.configurationmanager.url.views.systemconfigurationsview |
cfg/system |
URL path of the System Configurations view |
com.appjars.configurationmanager.url.views.usersconfigurationsview |
cfg/user/list |
URL path of the User Configurations view |
com.appjars.configurationmanager.url.views.myconfigurationsview |
cfg/user/my-configs |
URL path of the My Configurations view |
Date and Time Formats
| Property | Default | Description |
|---|---|---|
com.appjars.configurationmanager.dateformat |
dd-MM-yy |
Pattern used to render date values |
com.appjars.configurationmanager.datetimeformat |
dd-MM-yyyy HH:mm:ss |
Pattern used to render creation timestamps |
Application Restart
| Property | Default | Description |
|---|---|---|
com.appjars.configurationmanager.restart.enabled |
false |
Enables the Reload configuration button and the ApplicationRestarter service |
com.appjars.configurationmanager.restart.reloadDelayMillis |
5000 |
Milliseconds the browser waits before reloading after a restart is triggered |
Required Integration: UserProvider
Configuration Manager requires an implementation of UserProvider to identify the currently authenticated user and retrieve the full list of application users. This is used by the administrator view to manage per-user values and by the user view to display the current user's own settings.
public interface UserProvider {
String getPrincipalUsername();
List<String> getAllUsernames();
}
The appjar does not ship an implementation, so the host application must provide exactly one:
@Component
public class AppUserProvider implements UserProvider {
@Autowired
private UserService userService;
@Override
public String getPrincipalUsername() {
return SecurityContextHolder.getContext().getAuthentication().getName();
}
@Override
public List<String> getAllUsernames() {
return userService.findAll().stream()
.map(UserDto::getUsername)
.collect(Collectors.toList());
}
}
Annotate the bean with @Primary only if the application declares more than one UserProvider, so that Spring knows which one to inject.
Resolving Configurations from the Database
Configuration Manager installs itself as a Spring PropertySource named systemConfigurations, backed by the system configurations stored in the database. Any property placeholder the application resolves — through @Value, Environment.getProperty() or application.properties interpolation — can therefore be satisfied by a system configuration whose Name matches the property key.
@Value("${com.myapp.report.pageSize:20}")
private int pageSize;
As long as a system configuration named com.myapp.report.pageSize exists, its current value replaces the fallback 20.
Precedence
The property source is inserted immediately below systemEnvironment, which places the database between the operating system and the application's own files:
flowchart TD
A["Command-line arguments"] --> B["JVM system properties"]
B --> C["Environment variables"]
C --> D["System configurations (database)"]
D --> E["application.properties / application.yml"]
E --> F["@Value default, after the colon"]
A value stored in the database therefore overrides application.properties, while command-line arguments, JVM system properties and environment variables still override the database. This ordering lets an operator force a value for a single deployment without editing the database.
Two lookups are deliberately excluded. Keys beginning with spring.datasource. are never resolved from the database, because the property source needs those keys to open its own connection; and while the source is loading, lookups triggered by that load fall through to the sources below it. Both rules exist to prevent infinite recursion during bootstrap.
Caching and Invalidation
Values are read from the database once and cached in memory. Writing a system configuration through the service layer invalidates the cache, so the next lookup reloads from the database.
Invalidating the cache is not the same as updating the application. A @Value field is resolved when its bean is created, so beans that already exist keep the value they were injected with. Code that reads a property dynamically — through Environment.getProperty() — sees the new value immediately.
Applying New Values to Injected Beans
To make new values reach @Value fields, the application context must be rebuilt. ApplicationRestarter does this in place:
public interface ApplicationRestarter {
// Whether restarting is enabled by configuration and supported by the current runtime
boolean isEnabled();
// Close the current context and re-run the captured SpringApplication on a background thread
void restart();
}
restart() closes the running context and re-runs the SpringApplication captured at bootstrap, so every bean is recreated against a fresh environment. It returns immediately and completes asynchronously.
This is an opt-in, disruptive operation: it drops all active sessions. It is disabled unless com.appjars.configurationmanager.restart.enabled is true, and it is only supported when the application was launched through SpringApplication.run(...), which is the case for an executable JAR with an embedded server. isEnabled() returns false otherwise, and the Reload configuration button is hidden.
Note
Nested placeholders inside spring.datasource.* values are resolved from the sources below the database, never from the database itself.
System vs User Configurations
| System Configuration | User Configuration | |
|---|---|---|
| Scope | One active value shared across all users | Each user holds their own value, falling back to the default |
| Current value | Newest entry in cm_sc_history, or the default |
Newest entry in cm_uc_history for that user, or the default |
| Managed by | Administrators only | Administrators define the key; users set their own values |
| Views | System Configurations | User Configurations (admin), My Configurations (user) |
| Resolves placeholders | Yes, through the systemConfigurations property source |
No, read through the service API |
Service API
SystemConfigurationService
// Current value of a configuration: newest history entry, or the default when the history is empty
Optional<ConfigurationValueDto> findConfigValue(String configName);
// Same as findConfigValue, converted to the expected type
<T> Optional<T> getConfigValue(String configName, Class<T> type);
// Append a new value to the history; the newest entry becomes the current one
void addValue(SystemConfigurationDto config, ConfigurationValueDto value);
// Discard the current value and revert to the default
void restoreDefaults(SystemConfigurationDto config, String editorUsername);
An empty Optional from getConfigValue means either that the configuration does not exist or that its current value is null. Use findConfigValue when the two cases must be told apart. The method throws ClassCastException if the configuration's actual type is not compatible with the requested one.
UserConfigurationService
// Current value for a given user: newest entry in that user's history, or the default
Optional<ConfigurationValueDto> findConfigValueByUser(String configName, String username);
// Same as findConfigValueByUser, converted to the expected type
<T> Optional<T> getConfigValueByUser(String configName, String username, Class<T> type);
// All user configurations, with values filtered to a specific user
List<UserConfigurationDto> findByUsername(String username);
// Append a new value for a user; previous values are retained in the history
void addValueForUser(UserConfigurationDto config, ConfigurationValueDto value);
// Discard a user's values and revert that user to the default
void resetDefaultConfigForUser(UserConfigurationDto config, String username, String editorUsername);
addValueForUser throws IllegalArgumentException when the value carries no owner username.
Consuming Configurations in Application Code
For system configurations, the choice is between static injection and dynamic reading:
Static injection — resolved once, when the bean is created. Requires a restart to pick up new values:
@Value("${com.myapp.report.outputFormat:PDF}")
private String outputFormat;
Dynamic reading — re-evaluated on every call, so a saved value takes effect immediately:
@Autowired
private Environment environment;
public String getOutputFormat() {
return environment.getProperty("com.myapp.report.outputFormat", "PDF");
}
User configurations are not exposed as properties, because a property source is global while these values differ per user. Read them through the service:
@Autowired
private UserConfigurationService userConfigurationService;
public int getPageSize(String username) {
return userConfigurationService
.getConfigValueByUser("com.myapp.ui.pageSize", username, Integer.class)
.orElse(20);
}
Customisation
Assigning a Router Layout
By default, Configuration Manager registers its views without a parent layout. To wrap them in the application's main layout, inject the RouteConfigurer bean using its qualifier and call setViewsRouterLayout in a @PostConstruct method:
@Autowired
@Qualifier("ConfigurationManagerRouteConfigurer")
private RouteConfigurer routeConfigurer;
@PostConstruct
public void configure() {
routeConfigurer.setViewsRouterLayout(MainLayout.class);
}
The @Qualifier is required because multiple AppJars may contribute a bean named RouteConfigurer.
Customising View URLs
The default URL paths can be overridden in application.properties:
com.appjars.configurationmanager.url.views.systemconfigurationsview=myapp/system-config
com.appjars.configurationmanager.url.views.usersconfigurationsview=myapp/user-configs
com.appjars.configurationmanager.url.views.myconfigurationsview=myapp/my-config