Problem
Liquibase has an expanding set of global configuration options (e.g. duplicateFileMode, logLevel). Currently, these can only be set via environment variables or JVM system properties (e.g. -Dliquibase.duplicateFileMode=WARN).

In a Spring Boot application, this creates inconsistency. Most Liquibase settings live under spring.liquibase.*(application.yml) but in order to set global properties we must set global values elsewhere, such as through system properties, env vars, or through Customizer<Liquibase>. In my own experience, this fragmented approach made it difficult to configure liquibase across environments and made testing more difficult.

Use case
My personal use case was that we had multiple Liquibase changelog files and frequently saw an error indicating that Liquibase found duplicates. The recommended fix is to set liquibase.duplicateFileMode=WARN. The workaround we used was to set the variable in our Gradle file, which worked but led to issues I mentioned above.

Proposal
Introduce a generic pass-through mechanism for Liquibase global properties. This was the proposed approach on the long-standing Liquibase issue here. For example:

spring:
  liquibase:
    change-log: classpath:/db/changelog_master.xml
    globals:
      duplicate-file-mode: WARN
      log-level: INFO

Behavior:

  • At startup, entries under spring.liquibase.globals.* are applied to Liquibase’s global configuration before SpringLiquibase executes (internally via the existing customizer / LiquibaseConfiguration bridge).
  • Spring Boot does not hard-code or validate the option names; it simply passes them through. Liquibase remains the source of truth for supported keys and semantics.

Benefits

  • Keeps all Liquibase config in application.yml as the single source of truth
  • Reduces production/test mismatches.
  • Minimal maintenance burden: Spring Boot simply passes properties through; Liquibase defines the option set.

Proof of concept

I have a Kotlin project where I constructed a solution for this. It works, though there might be a better place to hook in to access the spring configuration. Note that I am not using spring.liquibase.globals because spring.liquibase maps to a static properties class. To implement that behavior, you'd need to add globals as a string map property. If this looks like something that your team would be interested in, I'd be happy to do the implementation or adapt it to be more consistent with the integration code.

src/main/resources/META-INF/services/liquibase.configuration.ConfigurationValueProvider

com.example.liquibase.config.SpringLiquibaseGlobalConfigProvider

src/main/resources/META-INF/spring.factories

org.springframework.context.ApplicationContextInitializer=\
com.example.liquibase.config.LiquibaseEnvironmentInitializer

src/main/kotlin/com/example/liquibase/config/LiquibaseEnvironmentInitializer.kt

package com.example.liquibase.config

import org.springframework.context.ApplicationContextInitializer
import org.springframework.context.ConfigurableApplicationContext
import org.springframework.core.Ordered

class LiquibaseEnvironmentInitializer :
    ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {

    override fun initialize(applicationContext: ConfigurableApplicationContext) {
        SpringLiquibaseGlobalConfigProvider.environment = applicationContext.environment
    }

    override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE
}

src/main/kotlin/com/example/liquibase/config/SpringLiquibaseGlobalConfigProvider.kt

package com.example.liquibase.config

import liquibase.configuration.AbstractConfigurationValueProvider
import liquibase.configuration.ProvidedValue
import org.springframework.core.env.ConfigurableEnvironment

class SpringLiquibaseGlobalConfigProvider : AbstractConfigurationValueProvider() {

    companion object {
        var environment: ConfigurableEnvironment? = null
    }

    override fun getPrecedence() = 10_000

    override fun getProvidedValue(vararg keyAndAliases: String?): ProvidedValue? {
        val env = environment ?: return null
        for (key in keyAndAliases.filterNotNull()) {
            val shortKey = key.substringAfterLast(".")
            val candidates = listOf(
                "app.liquibase.global-config.$shortKey",
                "app.liquibase.global-config.${toKebabCase(shortKey)}"
            )
            for (candidate in candidates) {
                env.getProperty(candidate)?.let { value ->
                    return ProvidedValue(key, key, value, "Spring Environment ($candidate)", this)
                }
            }
        }
        return null
    }

    private fun toKebabCase(s: String): String =
        s.foldIndexed(StringBuilder()) { i, acc, ch ->
            if (ch.isUpperCase() && i > 0) acc.append('-')
            acc.append(ch.lowercaseChar())
        }.toString()
}