Background
okapi-spring-boot's OutboxAutoConfiguration.PostgresStoreConfiguration registers a SpringLiquibase bean named okapiPostgresLiquibase to apply okapi's bundled changelog. The factory is conditional:
@Bean("okapiPostgresLiquibase")
@ConditionalOnClass(SpringLiquibase::class)
@ConditionalOnBean(value = [DataSource::class, PostgresOutboxStore::class])
@ConditionalOnMissingBean(name = ["okapiPostgresLiquibase"])
fun okapiPostgresLiquibase(): SpringLiquibase = ...
The architectural concern: this is a SpringLiquibase bean. Spring Boot's LiquibaseAutoConfiguration uses @ConditionalOnMissingBean(SpringLiquibase::class) (type match) on its own liquibase factory. When okapiPostgresLiquibase is registered, Spring Boot's liquibase is silently skipped, and the application's own changelog never runs.
For applications using Spring Boot's auto-configured Liquibase to apply their own migrations (test setups, dev environments, anywhere spring.liquibase.enabled=true), there's no clean way to opt out — only fragile workarounds.
Empirical observation
In our setup (Spring Boot 3.5.7 + okapi 0.2.0 + plain okapi-spring-boot + okapi-postgres + okapi-http) the @ConditionalOnBean(PostgresOutboxStore::class) condition on the okapiPostgresLiquibase factory evaluates as not-present, even though outboxStore is declared as a @Bean in the same inner @Configuration. The result is that okapiPostgresLiquibase is never registered, and:
- Spring Boot's
liquibase runs fine ✓
- okapi's changelog is never applied ✗ —
outbox table is missing after startup unless the application runs okapi's changelog itself
This appears to be a separate bug (intra-@Configuration @ConditionalOnBean evaluation order) that warrants its own investigation. Whichever way it gets resolved upstream, applications need a deterministic way to control the bean's presence — hence the requested opt-out.
Two failure modes from one root cause
Depending on Spring's evaluation order in a given setup, applications today see one of two bad outcomes:
okapiPostgresLiquibase registers → Spring Boot's liquibase shadowed → application changelog skipped, half-migrated schema.
okapiPostgresLiquibase does not register (our setup) → okapi changelog never applied, outbox table missing.
Neither is acceptable, and both are non-obvious to debug. There's no startup warning, no error — the application just silently misbehaves.
Reproducing the architectural concern
Minimal reproducer for failure mode 1:
// Spring Boot 3.5.x app with:
// spring.liquibase.enabled: true
// spring.liquibase.change-log: classpath:/db/changelog/changelog-master.xml
// org.liquibase:liquibase-core on the classpath
// Add okapi-bom + okapi-core + okapi-postgres + okapi-http + okapi-spring-boot.
// Provide an HttpMessageDeliverer bean.
If okapiPostgresLiquibase is registered (as the design intends), databasechangelog after startup contains only okapi's 001..004 entries — application changesets are missing, because Spring Boot's liquibase was skipped.
Workarounds today
To get both changelogs running, applications can either:
Option A — Run okapi's changelog programmatically via a standalone @Component:
@Component
internal class OkapiLiquibaseRunner(
private val dataSource: DataSource,
appLiquibase: ObjectProvider<SpringLiquibase>,
) : InitializingBean {
init { appLiquibase.ifAvailable { /* force ordering after Spring Boot's bean */ } }
override fun afterPropertiesSet() {
SpringLiquibase().apply {
this.dataSource = this@OkapiLiquibaseRunner.dataSource
this.changeLog = "classpath:com/softwaremill/okapi/db/changelog.xml"
}.afterPropertiesSet()
}
}
Option B — Include okapi's changelog from the application's own master changelog:
<!-- changelog-master.xml -->
<include file="com/softwaremill/okapi/db/changelog.xml"/>
Both options bypass okapi's auto-configured bean entirely. Both require every consumer to (a) discover the issue and (b) re-implement the bypass.
Proposed fix
Two complementary changes on OutboxAutoConfiguration / its inner PostgresStoreConfiguration:
1. Opt-out property
@Bean("okapiPostgresLiquibase")
@ConditionalOnClass(SpringLiquibase::class)
@ConditionalOnBean(value = [DataSource::class, PostgresOutboxStore::class])
@ConditionalOnMissingBean(name = ["okapiPostgresLiquibase"])
@ConditionalOnProperty(
prefix = "okapi.liquibase",
name = ["enabled"],
havingValue = "true",
matchIfMissing = true, // backward compatible: default behavior unchanged
)
fun okapiPostgresLiquibase(): SpringLiquibase = ...
(Same for the MySQL counterpart.)
Consumers using their own Liquibase setup can then opt out cleanly:
okapi:
liquibase:
enabled: false
…and apply okapi's changelog via either of the workaround options above.
2. Deterministic ordering relative to LiquibaseAutoConfiguration
Even with the opt-out, default enabled=true behavior could still cause shadowing, because auto-config processing falls back to alphabetical class-name order when no explicit ordering is specified (com.softwaremill.okapi… < org.springframework.boot.autoconfigure…). With okapi's auto-config processed first, okapiPostgresLiquibase would be registered before LiquibaseAutoConfiguration is evaluated → Spring Boot's liquibase skipped.
@AutoConfiguration
@AutoConfigureAfter(LiquibaseAutoConfiguration::class)
class OutboxAutoConfiguration { ... }
This makes the order deterministic. Spring Boot's liquibase is registered first, then okapi's bean evaluates @ConditionalOnMissingBean(name = ["okapiPostgresLiquibase"]) (by name) — liquibase doesn't match by name, so okapi's bean also registers. Both coexist, each runs its own changelog.
Combined with #37 (dedicated tracking tables), new applications dropping in okapi-spring-boot would coexist cleanly with their existing Spring Boot Liquibase setup — no opt-out workaround required.
Backward compatibility
@ConditionalOnProperty with matchIfMissing = true keeps the current default behavior. No existing consumer needs changes.
@AutoConfigureAfter(LiquibaseAutoConfiguration::class) is purely an ordering hint — it changes when the configuration class is processed, not which beans are registered.
Prior art
A library previously used in our codebase (corify-outbox) avoided this entire class of conflict by registering its SpringLiquibase programmatically via BeanDefinitionRegistry from a SmartInitializingSingleton, after Spring Boot's LiquibaseAutoConfiguration had completed. This bypasses Spring's @ConditionalOnMissingBean mechanics entirely. It also configured dedicated databaseChangeLogTable / databaseChangeLogLockTable (see #37 for the equivalent ask):
@Component
internal class LiquibaseConfiguration(
private val context: ConfigurableApplicationContext,
private val outboxDataSources: List<OutboxDataSource>,
) : SmartInitializingSingleton {
override fun afterSingletonsInstantiated() {
outboxDataSources.forEach { (dataSourceBeanName, dataSourceInstance) ->
createLiquibaseBean(dataSourceInstance, dataSourceBeanName)
}
}
private fun createLiquibaseBean(dataSource: DataSource, dataSourceBeanName: String) {
val beanDefinition = RootBeanDefinition(SpringLiquibase::class.java)
.apply { scope = BeanDefinition.SCOPE_SINGLETON }
beanDefinition.propertyValues.add("dataSource", dataSource)
beanDefinition.propertyValues.add("changeLog", "classpath:liquibase/changelog.xml")
beanDefinition.propertyValues.add("databaseChangeLogTable", "outbox_liquibase_changelog")
beanDefinition.propertyValues.add("databaseChangeLogLockTable", "outbox_liquibase_changelog_lock")
(context as? GenericApplicationContext)?.registerBeanDefinition(
"outboxLiquibase-$dataSourceBeanName",
beanDefinition,
)
context.getBean("outboxLiquibase-$dataSourceBeanName", SpringLiquibase::class.java)
}
}
A simpler @ConditionalOnProperty + @AutoConfigureAfter would achieve the same coexistence without resorting to programmatic bean registration.
Background
okapi-spring-boot'sOutboxAutoConfiguration.PostgresStoreConfigurationregisters aSpringLiquibasebean namedokapiPostgresLiquibaseto apply okapi's bundled changelog. The factory is conditional:The architectural concern: this is a
SpringLiquibasebean. Spring Boot'sLiquibaseAutoConfigurationuses@ConditionalOnMissingBean(SpringLiquibase::class)(type match) on its ownliquibasefactory. WhenokapiPostgresLiquibaseis registered, Spring Boot'sliquibaseis silently skipped, and the application's own changelog never runs.For applications using Spring Boot's auto-configured Liquibase to apply their own migrations (test setups, dev environments, anywhere
spring.liquibase.enabled=true), there's no clean way to opt out — only fragile workarounds.Empirical observation
In our setup (Spring Boot 3.5.7 + okapi 0.2.0 + plain
okapi-spring-boot+okapi-postgres+okapi-http) the@ConditionalOnBean(PostgresOutboxStore::class)condition on theokapiPostgresLiquibasefactory evaluates as not-present, even thoughoutboxStoreis declared as a@Beanin the same inner@Configuration. The result is thatokapiPostgresLiquibaseis never registered, and:liquibaseruns fine ✓outboxtable is missing after startup unless the application runs okapi's changelog itselfThis appears to be a separate bug (intra-
@Configuration@ConditionalOnBeanevaluation order) that warrants its own investigation. Whichever way it gets resolved upstream, applications need a deterministic way to control the bean's presence — hence the requested opt-out.Two failure modes from one root cause
Depending on Spring's evaluation order in a given setup, applications today see one of two bad outcomes:
okapiPostgresLiquibaseregisters → Spring Boot'sliquibaseshadowed → application changelog skipped, half-migrated schema.okapiPostgresLiquibasedoes not register (our setup) → okapi changelog never applied,outboxtable missing.Neither is acceptable, and both are non-obvious to debug. There's no startup warning, no error — the application just silently misbehaves.
Reproducing the architectural concern
Minimal reproducer for failure mode 1:
If
okapiPostgresLiquibaseis registered (as the design intends),databasechangelogafter startup contains only okapi's001..004entries — application changesets are missing, because Spring Boot'sliquibasewas skipped.Workarounds today
To get both changelogs running, applications can either:
Option A — Run okapi's changelog programmatically via a standalone
@Component:Option B — Include okapi's changelog from the application's own master changelog:
Both options bypass okapi's auto-configured bean entirely. Both require every consumer to (a) discover the issue and (b) re-implement the bypass.
Proposed fix
Two complementary changes on
OutboxAutoConfiguration/ its innerPostgresStoreConfiguration:1. Opt-out property
(Same for the MySQL counterpart.)
Consumers using their own Liquibase setup can then opt out cleanly:
…and apply okapi's changelog via either of the workaround options above.
2. Deterministic ordering relative to
LiquibaseAutoConfigurationEven with the opt-out, default
enabled=truebehavior could still cause shadowing, because auto-config processing falls back to alphabetical class-name order when no explicit ordering is specified (com.softwaremill.okapi…<org.springframework.boot.autoconfigure…). With okapi's auto-config processed first,okapiPostgresLiquibasewould be registered beforeLiquibaseAutoConfigurationis evaluated → Spring Boot'sliquibaseskipped.This makes the order deterministic. Spring Boot's
liquibaseis registered first, then okapi's bean evaluates@ConditionalOnMissingBean(name = ["okapiPostgresLiquibase"])(by name) —liquibasedoesn't match by name, so okapi's bean also registers. Both coexist, each runs its own changelog.Combined with #37 (dedicated tracking tables), new applications dropping in
okapi-spring-bootwould coexist cleanly with their existing Spring Boot Liquibase setup — no opt-out workaround required.Backward compatibility
@ConditionalOnPropertywithmatchIfMissing = truekeeps the current default behavior. No existing consumer needs changes.@AutoConfigureAfter(LiquibaseAutoConfiguration::class)is purely an ordering hint — it changes when the configuration class is processed, not which beans are registered.Prior art
A library previously used in our codebase (
corify-outbox) avoided this entire class of conflict by registering itsSpringLiquibaseprogrammatically viaBeanDefinitionRegistryfrom aSmartInitializingSingleton, after Spring Boot'sLiquibaseAutoConfigurationhad completed. This bypasses Spring's@ConditionalOnMissingBeanmechanics entirely. It also configured dedicateddatabaseChangeLogTable/databaseChangeLogLockTable(see #37 for the equivalent ask):A simpler
@ConditionalOnProperty+@AutoConfigureAfterwould achieve the same coexistence without resorting to programmatic bean registration.