Files
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

251 lines
23 KiB
Plaintext

revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
=== JpaAdapterComponentsConfig package scan ===
1 package dev.caskeleton.adapter.outbound.persistence.config;
2
3 import org.springframework.context.annotation.ComponentScan;
4 import org.springframework.context.annotation.Configuration;
5
6 /**
7 * Registers the JPA adapter's scanned components, which nothing registered (JPA-INT-006).
8 *
9 * <p>The composition root's {@code @ComponentScan} excludes {@code
10 * dev.caskeleton.adapter.outbound.persistence.**} by regex, and that exclusion is correct: it is
11 * what makes an optional capability optional, so a deployment with JPA off assembles no persistence
12 * beans rather than assembling them and hoping each one remembered to carry the switch.
13 *
14 * <p>What was missing is the other half. Eight classes in this leaf are written as scanned
15 * components — {@code SpringTransactionPort}, {@code PersistenceExceptionTranslator}, {@code
16 * StandardSqlStateErrorMapping}, {@code DomainContextAuditContextPort}, the idempotency store and
17 * its reaper, and the outbox store and its reaper — and once the broad scan stopped reaching them,
18 * nothing else did. They are annotated {@code @Component} and {@code @Repository} and were beans in
19 * no running application: {@code TransactionPort} in particular had no implementation at all, so
20 * every use case that opens a transaction had no port to open it with.
21 *
22 * <p>It surfaced as an unsatisfied dependency the first time a capability that needs a transaction
23 * was actually assembled — the notification orchestrator, in the local-notification-ingest lane —
24 * rather than as anything a unit test could see, because each of these classes is constructed
25 * directly by its own tests.
26 *
27 * <p>So the scan is restored, narrowed to the packages it should always have covered and reachable
28 * only through {@code PersistenceJpaRootAutoConfiguration}, which carries the JPA master switch.
29 * Off is still structural.
30 *
31 * <p>Two packages are deliberately absent:
32 *
33 * <ul>
34 * <li>{@code ..persistence.fileserver} — gated on its own capability switch, scanned by {@link
35 * dev.caskeleton.adapter.outbound.persistence.fileserver.FileserverJpaPersistenceConfig};
36 * <li>{@code ..persistence.notification} — assembled explicitly, bean by bean, by {@code
37 * NotificationJpaPersistenceFacade}.
38 * </ul>
39 *
40 * <p>Components under these packages keep their own {@code @ConditionalOnProperty} guards; being
41 * scanned makes them candidates, not unconditional beans.
42 *
43 * <p>{@code ..persistence.lock} is in the list for the same reason and with the same history.
44 * {@code DistributedLockPersistenceConfig} owns both lock providers — the in-process registry and
45 * the JDBC one — and was registered by nothing but a test calling {@code ctx.register(...)}. So a
46 * single-instance deployment had no {@code DistributedLockPort} at all, and a multi-instance one
47 * could not start: the composition root's own {@code DistributedLockConfig} asks for a bean
48 * qualified {@code jdbcDistributedLock} that only that configuration declares.
49 *
50 * <p>Each package's {@code @ConfigurationProperties} type is enabled by a configuration inside that
51 * same package — {@code JpaTransactionConfig} for {@code JpaTransactionSettings}, {@code
52 * DistributedLockPersistenceConfig} for {@code LockSettings} — rather than from here. Enabling them
53 * centrally would give {@code config} an edge to {@code lock} and {@code transaction} that the
54 * module map does not grant it, and the map is right: this class knows which packages to scan, not
55 * what is inside them. They need enabling at all because {@code @ConfigurationPropertiesScan}
56 * excludes this tree as deliberately as {@code @ComponentScan} does.
57 */
58 @Configuration(proxyBeanMethods = false)
59 @ComponentScan(
60 basePackages = {
61 "dev.caskeleton.adapter.outbound.persistence.audit",
62 "dev.caskeleton.adapter.outbound.persistence.failure",
63 "dev.caskeleton.adapter.outbound.persistence.idempotency",
64 "dev.caskeleton.adapter.outbound.persistence.lock",
65 "dev.caskeleton.adapter.outbound.persistence.outbox",
66 "dev.caskeleton.adapter.outbound.persistence.transaction"
67 })
68 public class JpaAdapterComponentsConfig {}
=== runtime-role policy production callers ===
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/PostgreSqlRuntimeRoleVerifier.java:57: policy.requireSafe(verify(dataSource));
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java:19:public record DatabaseRolePolicy(Set<String> allowedRoles, SearchPathPolicy searchPathPolicy) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java:55: searchPathPolicy.requireSafe(report.searchPath());
=== runtime-role policy all callers ===
src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/PostgreSqlSecurityContractTest.java:89: new DatabaseRolePolicy(
src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/PostgreSqlSecurityContractTest.java:92: new PostgreSqlRuntimeRoleVerifier().requireSafe(runtime, policy);
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:15: new DatabaseRolePolicy(
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:21: policy.requireSafe(new DatabasePrivilegeReport("app_runtime", "app, pg_catalog", false, false));
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:29: policy.requireSafe(new DatabasePrivilegeReport("app_runtime", "app", true, false)))
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:38: () -> policy.requireSafe(new DatabasePrivilegeReport("postgres", "app", false, false)))
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:48: policy.requireSafe(
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:57: policy.requireSafe(new DatabasePrivilegeReport("app_runtime", "\"$user\", app", false, false));
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/PostgreSqlRuntimeRoleVerifier.java:57: policy.requireSafe(verify(dataSource));
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java:19:public record DatabaseRolePolicy(Set<String> allowedRoles, SearchPathPolicy searchPathPolicy) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java:55: searchPathPolicy.requireSafe(report.searchPath());
=== runtime role advertised Stable + report semantics ===
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:61: CapabilitySupport.stable(JpaCapability.RUNTIME_ROLE_VERIFICATION),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:112: return roleVerifier.verify(dataSource);
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:25: boolean runtimeRoleVerified,
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:56: privileges != null && !privileges.holdsCreatePrivilege(),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:62: return openInViewDisabled && runtimeRoleVerified;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:194: public InitializingBean jpaPlatformStartupCheck(
=== startup contract docs ===
## Startup verification
`PostgreSqlRuntimeRoleVerifier` asks the *server* what the connection can do:
```sql
select current_user,
current_setting('search_path'),
has_schema_privilege(current_user, current_schema(), 'CREATE'),
has_database_privilege(current_user, current_database(), 'CREATE')
```
Configuration cannot answer this. Effective privileges come from direct grants, inherited role
memberships, `PUBLIC` grants, and schema ownership, and no reading of a deployment manifest
reconstructs that combination reliably.
Startup fails when the runtime role is not on the allowlist, or holds `CREATE` on the schema or the
database.
## search_path
`SearchPathPolicy` is an allowlist. `search_path` decides which schema an unqualified name resolves
to, so a writable untrusted schema on it — classically `public`, where `CREATE` was granted broadly
before PostgreSQL 15 — lets a planted table, function, or operator shadow the real one, and the
application executes it without noticing. `$user` is exempt: only the connected role owns it.
Refusing the runtime role `CREATE` closes the same route from the other side.
### 41.2 search_path
Application role의 `search_path`를 고정하고 untrusted schema의 object resolution을 차단한다. startup verifier가 current_user, current_schema, search_path, schema CREATE privilege를 검사한다.
### 41.3 Injection 방어
```text
JPQL/Native values → parameter binding
Dynamic sort → allowlist
Dynamic table/column → enum/catalog mapping만
Entity → API mass binding 금지
```
### 41.4 Secret
DB password는 secret manager/workload identity에서 주입하고 config·log·metric에 기록하지 않는다.
---
## 42. Spring Boot AutoConfiguration
### 42.1 Properties
```yaml
backend:
jpa:
enabled: true
require-postgresql: true
open-in-view: false
schema-management: VALIDATE
transaction-profiles: {}
retry-profiles: {}
observability:
hibernate-statistics: true
sql-parameters: false
security:
verify-runtime-role: true
verify-search-path: true
```
### 42.2 Startup Failures
```text
spring.jpa.open-in-view=true
prod ddl-auto != validate/none
unsupported PostgreSQL version
runtime role has DDL privilege
migration checksum mismatch
required transaction profile timeout missing
Experimental module enabled without feature flag
```
=== outbox composition path ===
src/app-bootstrap/src/main/resources/application.yml:662: # metrics, or store binding at all; relay-enabled below only decides whether the scheduler runs.
src/app-bootstrap/src/main/resources/application.yml:666: relay-enabled: ${APP_OUTBOX_RELAY_ENABLED:false}
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxMetrics.java:5:import dev.caskeleton.application.outbox.OutboxStorePort;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxMetrics.java:30: private final OutboxStorePort store;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxMetrics.java:35: public OutboxMetrics(OutboxStorePort store, ObjectProvider<MeterRegistry> meterRegistryProvider) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayBrokerRequirementValidator.java:10: * <p>{@code ca-skeleton.outbox.relay-enabled} defaults to true and {@code app.messaging.broker}
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayBrokerRequirementValidator.java:23: private static final String RELAY_KEY = "ca-skeleton.outbox.relay-enabled";
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayScheduler.java:5:import dev.caskeleton.application.outbox.PublishPendingOutboxEventsUseCase;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayScheduler.java:19: private final PublishPendingOutboxEventsUseCase relayUseCase;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayScheduler.java:24: PublishPendingOutboxEventsUseCase relayUseCase, OutboxMetrics metrics, Clock clock) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:6:import dev.caskeleton.application.outbox.OutboxStorePort;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:7:import dev.caskeleton.application.outbox.PublishPendingOutboxEventsUseCase;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:24: * <p>{@code ca-skeleton.outbox.enabled} is the capability switch; {@code relay-enabled} only
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:52: name = "ca-skeleton.outbox.relay-enabled",
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:56: OutboxStorePort store,
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:64: PublishPendingOutboxEventsUseCase relayUseCase =
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:65: new PublishPendingOutboxEventsUseCase(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:95: OutboxStorePort store, ObjectProvider<MeterRegistry> meterRegistryProvider) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/activation/CapabilityDependencyValidator.java:50: boolean relay = isOn(environment, "ca-skeleton.outbox.relay-enabled");
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/activation/CapabilityDependencyValidator.java:67: "ca-skeleton.outbox.relay-enabled=true only starts the scheduler for a capability that is "
src/application-core/src/main/java/dev/caskeleton/application/inbox/InboxStorePort.java:22: InboxTransitionOutcome markDead(InboxOwner owner, Duration retention, OperationId operationId);
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:87: markFailed(item, "BATCH_BYTE_BUDGET_EXHAUSTED", now);
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:123: markFailed(item, "CLEANUP_ATTEMPT_FAILED", now);
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:140: markFailed(item, "ACTIVE_WRITER_LEASE", now);
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:206: private void markFailed(CleanupItem item, String reasonCode, Instant now) {
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:209: queue.markFailed(item, reasonCode, now.plus(retryBackoff));
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/CleanupQueue.java:21: void markFailed(CleanupItem item, String reasonCode, Instant nextAttemptAt);
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxRelayResult.java:7: * Result returned by {@link PublishPendingOutboxEventsUseCase} after one relay cycle: the number of
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:35:public class PublishPendingOutboxEventsUseCase
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:38: private final OutboxStorePort store;
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:60: public PublishPendingOutboxEventsUseCase(
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:61: OutboxStorePort store,
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:90: List<OutboxEvent> claimed = tx.inWrite(() -> store.claimBatch(batchSize, now, inFlightTimeout));
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:141: tx.inWrite(() -> store.markPublished(event.eventId()));
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:159: tx.inWrite(() -> store.markFailed(event.eventId(), nextAttemptAt));
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:175: tx.inWrite(() -> store.markDead(event.eventId()));
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:14:public interface OutboxStorePort {
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:27: List<OutboxEvent> claimBatch(int batchSize, Instant now, Duration inFlightTimeout);
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:35: void markPublished(String eventId);
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:45: void markFailed(String eventId, Instant nextAttemptAt);
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:54: void markDead(String eventId);
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxEvent.java:7: * Immutable view of an outbox event row returned by {@link OutboxStorePort#claimBatch}.
src/application-core/src/main/java/dev/caskeleton/application/outbox/v2/OutboxPollingDeliveryPortV2.java:9: List<ClaimedOutboxDelivery> claimBatch(OutboxDeliveryClaimRequest request);
src/application-core/src/main/java/dev/caskeleton/application/outbox/v2/OutboxPollingDeliveryPortV2.java:11: OutboxDeliveryTransitionOutcome markPublished(OutboxDeliveryTransition transition);
src/application-core/src/main/java/dev/caskeleton/application/outbox/v2/OutboxPollingDeliveryPortV2.java:16: OutboxDeliveryTransitionOutcome markDead(OutboxDeliveryTransition transition, String errorCode);
src/application-core/src/main/java/dev/caskeleton/application/idempotency/v2/IdempotencyStorePortV2.java:28: IdempotencyFailOutcome markFailed(
src/application-core/src/main/java/dev/caskeleton/application/idempotency/v2/IdempotencyExecutorV2.java:217: store.markFailed(
src/application-core/src/main/java/dev/caskeleton/application/idempotency/v2/IdempotencyExecutorV2.java:282: store.markFailed(
src/application-core/src/main/java/dev/caskeleton/application/notification/platform/callback/ProviderEventLedger.java:26: void markFailed(ProviderEventRecordId eventId, String errorCode);
src/application-core/src/main/java/dev/caskeleton/application/notification/platform/callback/ProviderEventProjectionService.java:58: ledger.markFailed(event.id(), "NO_PROJECTOR");
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaCleanupQueue.java:145: public void markFailed(CleanupItem item, String reasonCode, Instant nextAttemptAt) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:8:import dev.caskeleton.application.outbox.OutboxStorePort;
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:18: * JPA-backed {@link OutboxAppendPort} + {@link OutboxStorePort}. {@link #append} and the claim/mark
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:24:public class OutboxStoreAdapter implements OutboxAppendPort, OutboxStorePort {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:29: public OutboxStoreAdapter(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:53: public List<OutboxEvent> claimBatch(int batchSize, Instant now, Duration inFlightTimeout) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:72: public void markPublished(String eventId) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:84: public void markFailed(String eventId, Instant nextAttemptAt) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:99: public void markDead(String eventId) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java:350: public InboxTransitionOutcome markDead(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:297: public List<ClaimedOutboxDelivery> claimBatch(OutboxDeliveryClaimRequest request) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:313: public OutboxDeliveryTransitionOutcome markPublished(OutboxDeliveryTransition transition) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:366: public OutboxDeliveryTransitionOutcome markDead(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java:294: public IdempotencyFailOutcome markFailed(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/RecipientDeliveryJpaRepository.java:36: List<Object[]> claimBatch(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaRecipientLeaseStore.java:47: return recipients.claimBatch(now, limit, workerId, leaseUntil).stream()
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaProviderEventLedger.java:146: public void markFailed(ProviderEventRecordId eventId, String errorCode) {
=== durable/live adapter construction references ===
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/liveevent/JpaLiveEventReplayAdapter.java:54: public JpaLiveEventReplayAdapter(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/operation/DurableOperationStoreAdapter.java:39: public DurableOperationStoreAdapter(DurableOperationJpaRepository repository) {