Command: printf '%s\n' '--- canonical boundary claim ---'; sed -n '18,118p' src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java; printf '%s\n' '--- transaction guide ---'; cat docs/jpa/transaction-guide.md; printf '%s\n' '--- FullTransactionRetryCoordinator production refs ---'; git grep -n -w 'FullTransactionRetryCoordinator' -- 'src/**/src/main/java/**/*.java'; printf '%s\n' '--- coordinator execute call candidates ---'; git grep -n -E '\.(execute)\(' -- 'src/**/src/main/java/**/*.java' | grep -E 'retry|transaction|coordinator' || true
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:01:16Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Compares current source/document claims for the application PolicyTransactionPort stack versus the public JPA executor/coordinator stack, including exact production execute references. It identifies coexistence/drift but does not by itself prove external consumers are absent.
--- stdout/stderr ---
--- canonical boundary claim ---
import org.springframework.transaction.PlatformTransactionManager;

/**
 * Composes the transaction half of the platform (design §9.3, §17, §19).
 *
 * <p>Separate from {@link JpaPlatformAutoConfiguration} because it backs off separately. An
 * application that already installs its own {@link PlatformTransactionManager} — a JTA setup, a
 * chained manager across two data sources — must keep it, and the rest of the platform is still
 * useful to it. Folding the two together would make "I have my own transaction manager" mean "I get
 * none of the platform".
 *
 * <p>Plain construction rather than Spring auto-configuration: this repository's composition root
 * owns wiring, and an adapter leaf that auto-configured a transaction manager would silently
 * replace one the application had deliberately chosen.
 *
 * <p>There is no declarative retry annotation any more. {@code @RetryableJpaTransaction} lived in
 * the persistence leaf and documented itself as something an application service would put on its
 * own methods — which application-core cannot do without importing an outbound adapter and
 * inverting the dependency this architecture is built on. The canonical boundary is {@code
 * PolicyTransactionPort.inTransaction(TransactionRequest, Supplier)}; the retry coordinator below
 * is what implements it, not a second way to ask for the same thing.
 */
public final class JpaTransactionAutoConfiguration {

  /** How long a logical operation may keep retrying before the budget is spent regardless. */
  public static final Duration DEFAULT_MAX_RETRY_ELAPSED = Duration.ofSeconds(30);

  private final Clock clock;

  public JpaTransactionAutoConfiguration(Clock clock) {
    this.clock = Objects.requireNonNull(clock, "clock");
  }

  /**
   * The commit-phase classifier the evidence-aware transaction manager is built with.
   *
   * <p>The manager itself is constructed inside the persistence leaf, through {@code
   * EvidenceAwareJpaTransactionManager.standard(entityManagerFactory, clock)}. The composition root
   * deliberately does not do it here: that would require {@code jakarta.persistence} and {@code
   * org.hibernate} on the composition root's compile classpath, and this repository keeps ORM types
   * inside the persistence leaf. What the root owns is the decision — whether to install the
   * platform's manager at all — and the classifier it uses.
   */
  public CommitFailureClassifier commitFailureClassifier() {
    return CommitFailureClassifier.standard(clock);
  }

  /** The programmatic transaction boundary, with no vendor classification. */
  public SpringJpaTransactionExecutor transactionExecutor(
      PlatformTransactionManager transactionManager) {
    return new SpringJpaTransactionExecutor(transactionManager, clock);
  }

  /**
   * The programmatic transaction boundary that classifies its attempts with a vendor's SQLSTATEs.
   *
   * <p>The overload above builds the catalog-free chain, whose vendor stage returns the failure
   * unchanged. That is the honest answer when no vendor translator exists, and it is the wrong one
   * whenever a vendor is composed: a 40001 or 40P01 stays a raw {@code DataAccessException}, never
   * becomes a {@code JpaPersistenceException}, and so never reaches the retry coordinator's catch.
   * Contention then goes unretried in exactly the deployments that have a database.
   *
   * @param vendorFailures the composed vendor's SQLSTATE translator
   */
  public SpringJpaTransactionExecutor transactionExecutor(
      PlatformTransactionManager transactionManager, VendorFailureTranslator vendorFailures) {
    return new SpringJpaTransactionExecutor(
        transactionManager,
        clock,
        new PersistenceFailureTranslatorChain(
            OptimisticConflictTranslator.withoutCatalog(), vendorFailures));
  }

  /**
   * The retry coordinator for one retry profile.
   *
   * <p>The policy and the coordinator's budget are built from the <em>same</em> profile on purpose.
   * Configuring them independently is how a deployment ends up with a policy that says "retry" and
   * a budget that permits one attempt — which looks like retry being broken rather than like a
   * misconfiguration.
   */
  public FullTransactionRetryCoordinator retryCoordinator(
      SpringJpaTransactionExecutor executor,
      RetryProfile retryProfile,
      RetryEventListener listener) {
    return retryCoordinator(executor, DefaultJpaRetryPolicy.forProfile(retryProfile), listener);
  }

  /** The retry coordinator for an application-supplied policy. */
  public FullTransactionRetryCoordinator retryCoordinator(
      SpringJpaTransactionExecutor executor, JpaRetryPolicy policy, RetryEventListener listener) {
    return new FullTransactionRetryCoordinator(
        executor, policy, retrySleeper(), clock, DEFAULT_MAX_RETRY_ELAPSED, listener);
  }

  /** The production sleeper; tests substitute a recording one. */
  public RetrySleeper retrySleeper() {
    return new ThreadRetrySleeper();
  }
}
--- transaction guide ---
# Transaction Guide

Design §15-§20. What owns a transaction, what may be retried, and what must never be.

## The application service owns the boundary

Repository adapters do not open transactions. The use case does, through `TransactionPort` or
`JpaTransactionExecutor`, because the unit of work is a business decision and only the use case
knows where it starts and ends.

Open Session In View is off in every runtime profile. It is on by default in Spring Boot, which is
why `JpaDangerousConfigurationGuard` fails startup rather than trusting configuration review.

## Profiles

A `TransactionProfile` fixes propagation, isolation, timeout, read-only, and the retry budget. A
write profile must carry a positive finite timeout — the type refuses to represent one without —
because an unbounded write transaction holds a connection, its locks, and its row versions for as
long as one stuck statement takes.

`REQUIRES_NEW` is opt-in. It acquires a second physical connection while pinning the first, so a
profile using it must be paired with the pool-pressure evidence in design §38:

```text
maximumPoolSize >= (concurrent_threads x (1 + max_requires_new_depth)) + 1
```

## Retry is per use case, never per statement

`FullTransactionRetryCoordinator` re-enters the executor, which produces a new transaction and a new
Persistence Context for every attempt. That granularity is the whole point: an optimistic conflict
means the state the attempt computed against is no longer the committed state, so re-issuing the
same statement would compute the same wrong answer. The domain rules have to run again against
reloaded data.

Retryable: serialization failure (`40001`), deadlock (`40P01`), optimistic conflict.
Not retryable: constraint violations, schema mismatch, query timeout, and anything unclassified.

Two additional refusals, independent of budget:

- An attempt that declared an irreversible external effect through `IrreversibleSideEffectContext`.
  Rollback reverses database work only; an email or a card charge has already changed the world.
- Anything completion-unknown.

## Completion unknown

`TransactionCompletionUnknownException` is never retried, and the type system enforces it twice:
`JpaFailureContext` refuses to represent a retryable completion-unknown failure, and the exception
rebuilds its context through the safe factory whatever it is handed.

`EvidenceAwareJpaTransactionManager` marks the phase `COMMITTING` immediately before delegating to
the provider commit and never after. If the network, the JVM, or the server dies inside that call,
the last thing written is "we asked, we do not know" — which is exactly the state that must not be
mistaken for a rollback.

Recovery is reconciliation, not retry:

```text
record the transaction key -> check the idempotency record
                           -> check the business row
                           -> check the outbox
                           -> still undetermined? reconciliation queue
```

`CompletionUnknownRecorder` writes that record through a channel outside the unknown transaction.
Writing it through the same connection would make the audit trail share the failure it documents.
--- FullTransactionRetryCoordinator production refs ---
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:50:  public FullTransactionRetryCoordinator(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:21: * <p>This class does not retry. Retry lives in {@link FullTransactionRetryCoordinator}, which calls
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:170:  public FullTransactionRetryCoordinator jpaRetryCoordinator(
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:99:  public FullTransactionRetryCoordinator retryCoordinator(
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:109:    return new FullTransactionRetryCoordinator(
--- coordinator execute call candidates ---
src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/DefaultGenericHttpGateway.java:118:          HttpCallResult<T> result = coordinator.execute(call);
src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/webclient/DefaultReactiveHttpGateway.java:149:                coordinator.execute(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:101:            transactionExecutor.execute(operation, profile, work, attemptNumber, transactionKey);
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:83:        return template.execute(status -> work.get());
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:166:    return policyExecutor.execute(request, action);
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:197:      return template.execute(status -> action.get());
src/adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/transaction/SpringMongoTransactionExecutor.java:57:    return coordinator.execute(profile, work);

Exit code: 0
