Command: printf '%s\n' '--- persistence root ---'; sed -n '1,115p' src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/persistencejpa/PersistenceJpaRootAutoConfiguration.java; printf '%s\n' '--- adapter component scan ---'; cat src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/JpaAdapterComponentsConfig.java; printf '%s\n' '--- runtime transaction beans ---'; sed -n '140,195p' src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java; printf '%s\n' '--- canonical/application references ---'; git grep -n -E '\b(SpringTransactionPort|PolicyTransactionPort|FullTransactionRetryCoordinator|SpringJpaTransactionExecutor)\b' -- 'src/**/src/main/java/**/*.java' Working directory: /shared/codebase/clean-architecture-backend-template Executed at: 2026-08-29T08:01:16Z Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5 Observation boundary: Shows shipped persistence root imports, narrow adapter component scan, transaction bean factories, and exact production construction/reference sites for the two transaction stacks. Static wiring does not prove runtime invocation frequency. --- stdout/stderr --- --- persistence root --- package dev.caskeleton.bootstrap.autoconfigure.persistencejpa; import dev.caskeleton.adapter.outbound.persistence.config.JpaAdapterComponentsConfig; import dev.caskeleton.adapter.outbound.persistence.config.PersistenceVendorSettings; import dev.caskeleton.adapter.outbound.persistence.fileserver.FileserverJpaPersistenceConfig; import dev.caskeleton.adapter.outbound.persistence.h2.H2PersistenceConfig; import dev.caskeleton.adapter.outbound.persistence.notification.configuration.NotificationJpaPersistenceFacade; import dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlPersistenceConfig; import dev.caskeleton.bootstrap.autoconfigure.jpa.JpaDataSourceProfileValidator; import dev.caskeleton.bootstrap.autoconfigure.jpa.JpaPlatformRuntimeAutoConfiguration; import dev.caskeleton.bootstrap.autoconfigure.jpa.PostgreSqlVersionPolicy; import dev.caskeleton.bootstrap.runtime.startup.MigrationStartupConfig; import javax.sql.DataSource; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.core.env.Environment; /** * The one place that decides whether this application has relational persistence. * *

Every JPA configuration is reached through this import rather than through the component scan, * so a bean added to any of them next month is gated without anyone remembering to repeat a * condition. That is the difference between a capability that is off and a capability whose known * beans are individually conditioned. * *

Both vendor configurations are named here, and each is the leaf\'s real entry point: a vendor * config imports the core JPA config, not the reverse. Inverting that to give this class a single * import produced a package cycle inside the leaf, so the export surface admits the two vendor * packages instead. They are mutually exclusive on {@code ca-skeleton.persistence.vendor}, so * naming both lets the selector decide which one assembles while the decision about * whether there is a database stays here, in one place. * *

The notification facade is named here only now that the payload is protected at rest. * Importing it makes {@code JpaNotificationRequestStore} reachable, and that store used to write * the accepted request's template variables to {@code notification_request.variables_payload} in * plaintext — caller content that can be a reset code, an order total or an address (NTF-INT-007). * It was wired here once before that was true; the at-rest contract test failed on exactly that * fact, which is what it was written to do, and the wiring was reverted rather than the test * relaxed. The envelope came first, {@code NotificationRecordMapper} now takes the protection as a * required constructor argument, and only then does this import become a wiring rather than an * exposure. * *

{@code FileserverJpaPersistenceConfig} is named for a related reason. Its six entities map six * {@code fs_*} tables that live only in {@code db/migration/jpa/fileserver}, a stream applied only * when that capability is on, while the primary Flyway location creates none of them. They used to * be in the unconditional entity scan, so {@code ddl-auto=validate} against real PostgreSQL failed * on {@code fs_cleanup_item} in every deployment that had switched the capability off — which was * every JPA-on Compose lane. The scan now carries the capability's own condition, and this is the * only place that knows both that the capability is on and that a JPA vendor is composed. Neither * the leaf's {@code config} package nor a component scan can register it: {@code config} may depend * on {@code api} alone, and the whole persistence package is excluded from the root's scan. */ @AutoConfiguration @ConditionalOnProperty( prefix = "ca-skeleton.persistence-jpa", name = "enabled", havingValue = "true") @Import({ JpaAdapterComponentsConfig.class, PostgreSqlPersistenceConfig.class, H2PersistenceConfig.class, FileserverJpaPersistenceConfig.class, NotificationJpaPersistenceFacade.class, MigrationStartupConfig.class, JpaPlatformRuntimeAutoConfiguration.class }) public class PersistenceJpaRootAutoConfiguration { /** * Runs the datasource validator, which nothing used to run (JPA-INT-002). * *

{@code JpaDataSourceProfileValidator} was created as a bean and never invoked: both of its * methods were reachable only from their own unit test. A validator nobody calls is a comment — * and this one would have failed every deployment if called, because the namespace it read was * supplied by nothing. The two defects cancelled, which is the only reason the application * started. * *

Invoked from here rather than from the platform configuration so it runs exactly when JPA is * on and never when it is off, and as an {@link InitializingBean} so the failure lands during * startup rather than at whoever sends the first request. * *

The vendor is read from the {@link Environment} rather than injected as {@code * PersistenceVendorSettings}, because that type is not a registered bean here: the composition * root's {@code @ConfigurationPropertiesScan} deliberately excludes the persistence package so an * optional capability cannot bind — or reject — its detail settings in a deployment that never * switched it on. The default matches {@code PostgreSqlPersistenceConfig}'s {@code matchIfMissing * = true}: an unset selector is PostgreSQL, which is the vendor this configuration was * unconditional about before the selector existed. * *

The validator is constructed here rather than injected. It is stateless, and the bean that * used to supply it comes from {@code JpaPlatformRuntimeAutoConfiguration}, which carries * {@code @ConditionalOnBean(DataSource.class)} on a plain {@code @Configuration} imported by this * root — a condition evaluated during configuration-class parsing, before the datasource bean * definition is registered. That class therefore drops out silently in the real application, * taking the whole JPA add-on layer with it. Depending on a bean from it would make this check * disappear for the same reason the thing it checks disappeared. The condition-ordering defect is * recorded separately; it is not this task's to fix quietly. * * @param dataSource the resolved datasource, injected so the database checked is the one in use * @param environment the resolved environment, for the vendor selector * @return the startup check */ @Bean public InitializingBean jpaResolvedDataSourceCheck( DataSource dataSource, Environment environment) { JpaDataSourceProfileValidator validator = new JpaDataSourceProfileValidator(new PostgreSqlVersionPolicy()); String vendor = environment.getProperty(PersistenceVendorSettings.VENDOR_PROPERTY, "postgresql").trim(); return () -> validator.validateResolved(dataSource, "postgresql".equalsIgnoreCase(vendor)); } } --- adapter component scan --- package dev.caskeleton.adapter.outbound.persistence.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; /** * Registers the JPA adapter's scanned components, which nothing registered (JPA-INT-006). * *

The composition root's {@code @ComponentScan} excludes {@code * dev.caskeleton.adapter.outbound.persistence.**} by regex, and that exclusion is correct: it is * what makes an optional capability optional, so a deployment with JPA off assembles no persistence * beans rather than assembling them and hoping each one remembered to carry the switch. * *

What was missing is the other half. Eight classes in this leaf are written as scanned * components — {@code SpringTransactionPort}, {@code PersistenceExceptionTranslator}, {@code * StandardSqlStateErrorMapping}, {@code DomainContextAuditContextPort}, the idempotency store and * its reaper, and the outbox store and its reaper — and once the broad scan stopped reaching them, * nothing else did. They are annotated {@code @Component} and {@code @Repository} and were beans in * no running application: {@code TransactionPort} in particular had no implementation at all, so * every use case that opens a transaction had no port to open it with. * *

It surfaced as an unsatisfied dependency the first time a capability that needs a transaction * was actually assembled — the notification orchestrator, in the local-notification-ingest lane — * rather than as anything a unit test could see, because each of these classes is constructed * directly by its own tests. * *

So the scan is restored, narrowed to the packages it should always have covered and reachable * only through {@code PersistenceJpaRootAutoConfiguration}, which carries the JPA master switch. * Off is still structural. * *

Two packages are deliberately absent: * *

* *

Components under these packages keep their own {@code @ConditionalOnProperty} guards; being * scanned makes them candidates, not unconditional beans. * *

{@code ..persistence.lock} is in the list for the same reason and with the same history. * {@code DistributedLockPersistenceConfig} owns both lock providers — the in-process registry and * the JDBC one — and was registered by nothing but a test calling {@code ctx.register(...)}. So a * single-instance deployment had no {@code DistributedLockPort} at all, and a multi-instance one * could not start: the composition root's own {@code DistributedLockConfig} asks for a bean * qualified {@code jdbcDistributedLock} that only that configuration declares. * *

Each package's {@code @ConfigurationProperties} type is enabled by a configuration inside that * same package — {@code JpaTransactionConfig} for {@code JpaTransactionSettings}, {@code * DistributedLockPersistenceConfig} for {@code LockSettings} — rather than from here. Enabling them * centrally would give {@code config} an edge to {@code lock} and {@code transaction} that the * module map does not grant it, and the map is right: this class knows which packages to scan, not * what is inside them. They need enabling at all because {@code @ConfigurationPropertiesScan} * excludes this tree as deliberately as {@code @ComponentScan} does. */ @Configuration(proxyBeanMethods = false) @ComponentScan( basePackages = { "dev.caskeleton.adapter.outbound.persistence.audit", "dev.caskeleton.adapter.outbound.persistence.failure", "dev.caskeleton.adapter.outbound.persistence.idempotency", "dev.caskeleton.adapter.outbound.persistence.lock", "dev.caskeleton.adapter.outbound.persistence.outbox", "dev.caskeleton.adapter.outbound.persistence.transaction" }) public class JpaAdapterComponentsConfig {} --- runtime transaction beans --- } /** * The programmatic transaction boundary, over whichever manager the application installed. * *

The composed vendor's translator is taken when one exists. Without it the executor's chain * has no SQLSTATE stage, so a serialization failure or deadlock stays a raw {@code * DataAccessException}, never becomes the {@code JpaPersistenceException} the retry coordinator * catches, and contention is not retried at all. * * @param vendorFailures the composed vendor's SQLSTATE translator, when a vendor registered one */ @Bean @ConditionalOnMissingBean @ConditionalOnBean(PlatformTransactionManager.class) public SpringJpaTransactionExecutor jpaTransactionExecutor( PlatformTransactionManager transactionManager, Clock clock, ObjectProvider vendorFailures) { JpaTransactionAutoConfiguration composition = new JpaTransactionAutoConfiguration(clock); VendorFailureTranslator vendor = vendorFailures.getIfAvailable(); return vendor == null ? composition.transactionExecutor(transactionManager) : composition.transactionExecutor(transactionManager, vendor); } /** The whole-transaction retry coordinator. */ @Bean @ConditionalOnMissingBean @ConditionalOnBean(SpringJpaTransactionExecutor.class) public FullTransactionRetryCoordinator jpaRetryCoordinator( SpringJpaTransactionExecutor executor, RetryEventListener retryListener, Clock clock) { return new JpaTransactionAutoConfiguration(clock) .retryCoordinator( executor, dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryProfile .boundedContention("jpa-platform-default", 3), retryListener); } /** * The retry listener. * *

The interface's methods all have defaults, so an empty implementation is the honest "no * observer wired" value; an application that wants retry telemetry supplies its own bean. */ @Bean @ConditionalOnMissingBean public RetryEventListener jpaRetryEventListener() { return new RetryEventListener() {}; } /** Runs the dangerous-configuration guard during refresh. */ @Bean public InitializingBean jpaPlatformStartupCheck( JpaDangerousConfigurationGuard guard, Environment environment) { --- canonical/application references --- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/JpaAdapterComponentsConfig.java:15: * components — {@code SpringTransactionPort}, {@code PersistenceExceptionTranslator}, {@code src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:30:public final class FullTransactionRetryCoordinator { src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:32: private final SpringJpaTransactionExecutor transactionExecutor; src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:50: public FullTransactionRetryCoordinator( src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:51: SpringJpaTransactionExecutor transactionExecutor, src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionConfig.java:9: *

{@link SpringTransactionPort} takes it as a constructor argument, and the composition root's src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:21: *

This class does not retry. Retry lives in {@link FullTransactionRetryCoordinator}, which calls src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:25:public final class SpringJpaTransactionExecutor implements JpaTransactionExecutor { src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:32: public SpringJpaTransactionExecutor(PlatformTransactionManager transactionManager, Clock clock) { src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:41: public SpringJpaTransactionExecutor( src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:8:import dev.caskeleton.application.transaction.PolicyTransactionPort; src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:26: * Spring-backed {@link PolicyTransactionPort}: one pre-built {@link TransactionTemplate} per mode, src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:31:public class SpringTransactionPort implements PolicyTransactionPort { src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:39: public SpringTransactionPort(PlatformTransactionManager transactionManager) { src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:49: SpringTransactionPort( src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:61: public SpringTransactionPort( src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:77: SpringTransactionPort( src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:90: SpringTransactionPort( src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:105: SpringTransactionPort( src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:7:import dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator; src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:8:import dev.caskeleton.adapter.outbound.persistence.transaction.SpringJpaTransactionExecutor; src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:155: public SpringJpaTransactionExecutor jpaTransactionExecutor( src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:169: @ConditionalOnBean(SpringJpaTransactionExecutor.class) src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:170: public FullTransactionRetryCoordinator jpaRetryCoordinator( src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:171: SpringJpaTransactionExecutor executor, RetryEventListener retryListener, Clock clock) { src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:9:import dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator; src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:13:import dev.caskeleton.adapter.outbound.persistence.transaction.SpringJpaTransactionExecutor; src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:37: * PolicyTransactionPort.inTransaction(TransactionRequest, Supplier)}; the retry coordinator below src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:66: public SpringJpaTransactionExecutor transactionExecutor( src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:68: return new SpringJpaTransactionExecutor(transactionManager, clock); src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:82: public SpringJpaTransactionExecutor transactionExecutor( src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:84: return new SpringJpaTransactionExecutor( src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:99: public FullTransactionRetryCoordinator retryCoordinator( src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:100: SpringJpaTransactionExecutor executor, src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:107: public FullTransactionRetryCoordinator retryCoordinator( src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:108: SpringJpaTransactionExecutor executor, JpaRetryPolicy policy, RetryEventListener listener) { src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:109: return new FullTransactionRetryCoordinator( src/application-core/src/main/java/dev/caskeleton/application/transaction/PolicyTransactionPort.java:10:public interface PolicyTransactionPort extends TransactionPort { src/application-core/src/main/java/dev/caskeleton/application/transaction/TransactionPort.java:8: * {@code SpringTransactionPort}) implements it over Spring's {@code PlatformTransactionManager}. Exit code: 0