Repository navigation
Correlation ID Audit Trigger Deferred Binding and Test Hygiene
Type: Pattern Domain: Backend — persistence and testing Applies to: Spring Boot 4.1, Hibernate ORM 7.x, PostgreSQL 17 (pgjdbc), Micrometer Tracing, Testcontainers Enforced by: java-specialist (code review) Last updated: 2026-08-26 Status: Current
PostgreSQL GUCs set with set_config(..., true) are transaction-local. Bind them lazily — on first statement use, never at connection acquisition — and test the path with real tracer auto-configuration, explicit fixture transactions, and a shared database that stays intact.
CorrelationIdConnectionProvider decorates Hibernate's ConnectionProvider. On getConnection() it asks the delegate for a pooled connection and, when a Micrometer span carries a non-blank traceId, wraps it in a DeferredCorrelationIdConnection. The wrapper runs SELECT set_config('app.correlation_id', ?, true) exactly once, immediately before the FIRST createStatement, prepareStatement, prepareCall, or setSavepoint call.
The bind therefore lands AFTER Spring has applied transaction isolation, inside whatever transaction Spring/Hibernate established.
@Override
public Connection getConnection() throws SQLException {
Connection connection = delegate.getConnection();
Span span = tracer.currentSpan();
if (span == null || span.isNoop()) {
return connection;
}
String traceId = span.context().traceId();
if (traceId == null || traceId.isBlank()) {
return connection;
}
return new DeferredCorrelationIdConnection(connection, traceId);
}/** Executes the transaction-local GUC binding exactly once, before first use. */
private void bindOnce() throws SQLException {
if (bound) {
return;
}
bound = true;
try (PreparedStatement statement = target.prepareStatement(BIND_CORRELATION_ID)) {
statement.setString(1, traceId);
statement.execute();
}
}Binding eagerly broke every SERIALIZABLE service method. The eager implementation called setAutoCommit(false) and executed the set_config statement directly inside getConnection(). That opened the PostgreSQL transaction block BEFORE Spring ran SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, and PostgreSQL rejected the switch:
PSQLException: Cannot change transaction isolation level in the middle of a transaction
Hibernate acquires the connection before it applies the caller's isolation level. Any statement issued during acquisition runs outside the transaction the caller asked for.
Pool-wide spring.datasource.hikari.auto-commit=false is REQUIRED for this design. set_config(..., true) needs an active transaction block; autocommit-on single statements never have one.
spring:
datasource:
hikari:
auto-commit: falseWarning
The manual HibernatePropertiesCustomizer bean in GlobalConfiguration
(correlationIdConnectionProviderCustomizer) must stay as-is. It constructs
DatasourceConnectionProviderImpl directly, wraps it in
CorrelationIdConnectionProvider, and registers the result under
JdbcSettings.CONNECTION_PROVIDER via map customization.
Do not "simplify" it to bean-container SPI instantiation (Hibernate's
beans.container reflection lookups). The current wiring uses no reflection,
no proxies, no runtime bytecode, and is deliberately GraalVM/native-image safe.
Reflection-based service lookup is a known native-image failure source.
@Bean
@SuppressWarnings({"deprecation", "removal"})
public HibernatePropertiesCustomizer correlationIdConnectionProviderCustomizer(
DataSource dataSource, Tracer tracer) {
return hibernateProperties -> {
DatasourceConnectionProviderImpl delegate = new DatasourceConnectionProviderImpl();
delegate.setDataSource(dataSource);
CorrelationIdConnectionProvider provider = new CorrelationIdConnectionProvider(delegate, tracer);
provider.configure(hibernateProperties);
hibernateProperties.put(JdbcSettings.CONNECTION_PROVIDER, provider);
};
}CorrelationIdAuditIntegrationTest originally shipped a ~110-line ThreadLocalTracer implementing the full Micrometer SPI to fake span context. Replaced by @AutoConfigureTracing on AbstractTestContainerIntegrationTest:
import org.springframework.boot.micrometer.tracing.test.autoconfigure.AutoConfigureTracing;
@Testcontainers
@ExtendWith({SpringExtension.class})
@AutoConfigureTracing
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class AbstractTestContainerIntegrationTest {The annotation comes from org.springframework.boot:spring-boot-micrometer-tracing-test, test scope. @SpringBootTest supplies no-op tracing components unless this annotation enables real ones. See Spring Boot Tracing in Integration Tests for the diagnostic order.
Four rules from debugging this spike:
With auto-commit=false, a bare jdbcTemplate write outside an explicit transaction is rolled back when Hikari reclaims the connection. Wrap fixture and raw-SQL setup in TransactionTemplate so rows commit.
pgjdbc returns Java null for SQL NULL columns. Assert isNullOrEmpty(), not isEmpty() — calling isEmpty() on a null string throws before the real assertion runs.
The integration database is shared across all test classes in the suite. hrs.district_volume holds Flyway-seeded reference rows other tests depend on for grade validation. Deleting them breaks unrelated tests. Prevent duplicate-open-entry collisions instead: give test entities closed date ranges so two tests never hold open-ended rows for the same key.
Leftover target/surefire-reports and target/failsafe-reports from out-of-band runs can fail failsafe:verify even with green tests — failsafe-summary.xml carries old failure state. Run rm -rf target/surefire-reports target/failsafe-reports before diagnosing.
- A Hibernate
ConnectionProviderdecorator, or any acquisition-time hook, must set transaction-scoped state. - Integration tests exercise audit triggers that read session/GUC state.
- Several test classes share one Testcontainer database.
Eager binding is safe only when nothing configures the transaction after acquisition — no Spring isolation attributes and no SET TRANSACTION anywhere in the touched paths. This codebase has SERIALIZABLE service methods, so the deferred shape applies everywhere here.
A session-scoped GUC (set_config(..., false)) plus an explicit reset in closeConnection() solves the ordering problem differently but leaks state on any missed close path. Deviations from the deferred decorator require an ADR.
Code-review checks:
- A statement executed inside
getConnection()or any DataSource acquire path — verify it cannot precede Spring's isolation setup. - Hand-rolled tracer/SPI implementations under
src/test— replace with@AutoConfigureTracing. -
jdbcTemplatefixture writes without a wrapping transaction whileauto-commit=false. - Fixture deletes against
hrs.district_volume. - Failing
failsafe:verifywith green tests — check report-directory staleness first.
- Correlation ID Audit Propagation — the architecture this pattern implements.
- Spring Boot Tracing in Integration Tests — the no-op tracer gotcha behind Lesson 2.
- Tracing — B3 header forwarding in the application.
Code paths on main (merged via PR #1258):
backend/src/main/java/ca/bc/gov/nrs/hrs/security/CorrelationIdConnectionProvider.javabackend/src/main/java/ca/bc/gov/nrs/hrs/configuration/GlobalConfiguration.javabackend/src/main/resources/db/migration/V1.0.6__audit_correlation_id.sqlbackend/src/test/java/ca/bc/gov/nrs/hrs/extensions/AbstractTestContainerIntegrationTest.javabackend/src/test/java/ca/bc/gov/nrs/hrs/security/CorrelationIdAuditIntegrationTest.javabackend/src/test/java/ca/bc/gov/nrs/hrs/controller/DistrictVolumeCorrelationIdAuditIntegrationTest.java
This wiki documents NR Waste Plus for users and developers. Verify security and deployment guidance against the current application configuration before applying it elsewhere.
- Report an issue or ask a question in the GitHub repository.
- Follow the Contributing Guidelines to propose an update.
- Read the Code of Conduct.
Collection indexes support human browsing and machine retrieval. Keep both current when adding or moving wiki pages.
NR Waste Plus Wiki | GitHub Repository