Skip to content

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 : ""
Hold "Alt" / "Option" to enable pan & zoom

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 (an EnvironmentPostProcessor, in -data-impl) installs the database-backed property source into the Spring Environment.
  • SpringApplicationHolder (a SpringApplicationRunListener, in -business-impl) captures the running SpringApplication so 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"]
Hold "Alt" / "Option" to enable pan & zoom

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