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>
182 lines
15 KiB
Plaintext
182 lines
15 KiB
Plaintext
Command: static reachability/wiring/export/package evidence for hibernate, springdata, querydsl
|
|
Working directory: /shared/codebase/clean-architecture-backend-template
|
|
Executed at: 2026-08-29T08:22:05Z
|
|
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
Observation boundary: Tracked production Java/static configuration only. Zero references do not exclude reflection/external adopters; package export rules and test/integration consumers are shown separately.
|
|
--- stdout/stderr ---
|
|
=== package denominator ===
|
|
hibernate main count= 22 loc= 1111
|
|
hibernate test count= 2 loc= 149
|
|
springdata main count= 17 loc= 1028
|
|
springdata test count= 8 loc= 598
|
|
querydsl main count= 3 loc= 139
|
|
querydsl test count= 1 loc= 53
|
|
=== key implementation production refs ===
|
|
--- NamedStatementInspector
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/NamedStatementInspector.java:20:public final class NamedStatementInspector implements StatementInspector {
|
|
--- HibernateBatchConfigurationGuard
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/HibernateBatchConfigurationGuard.java:23:public final class HibernateBatchConfigurationGuard {
|
|
--- HibernateJpaBatchExecutor
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/HibernateJpaBatchExecutor.java:22:public final class HibernateJpaBatchExecutor implements JpaBatchExecutor {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/HibernateJpaBatchExecutor.java:27: public HibernateJpaBatchExecutor(
|
|
--- JpaBatchProfileRegistry
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/JpaBatchProfileRegistry.java:14:public final class JpaBatchProfileRegistry {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/JpaBatchProfileRegistry.java:18: public JpaBatchProfileRegistry(Map<String, JpaBatchProfile> profiles) {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/JpaBatchProfileRegistry.java:35: public static JpaBatchProfileRegistry of(JpaBatchProfile... profiles) {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/JpaBatchProfileRegistry.java:42: return new JpaBatchProfileRegistry(byName);
|
|
--- HibernateBulkDmlExecutor
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/bulk/HibernateBulkDmlExecutor.java:28:public final class HibernateBulkDmlExecutor implements BulkDmlExecutor {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/bulk/HibernateBulkDmlExecutor.java:38: public HibernateBulkDmlExecutor(
|
|
--- HibernateStatelessSessionRunner
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/stateless/HibernateStatelessSessionRunner.java:23:public final class HibernateStatelessSessionRunner implements StatelessSessionRunner {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/stateless/HibernateStatelessSessionRunner.java:28: public HibernateStatelessSessionRunner(
|
|
--- EntityGraphCatalog
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/EntityGraphCatalog.java:20:public final class EntityGraphCatalog {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/EntityGraphCatalog.java:25: public EntityGraphCatalog(
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/EntityGraphCatalog.java:40: public static EntityGraphCatalog empty() {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/EntityGraphCatalog.java:41: return new EntityGraphCatalog(Map.of());
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:30: private final EntityGraphCatalog catalog;
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:33: public FetchPlanApplier(EntityGraphCatalog catalog, EntityManager entityManager) {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:61: public EntityGraphCatalog catalog() {
|
|
--- FetchPlanApplier
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:22:public final class FetchPlanApplier {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:33: public FetchPlanApplier(EntityGraphCatalog catalog, EntityManager entityManager) {
|
|
--- JpaKeysetQuerySupport
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java:20:public final class JpaKeysetQuerySupport {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java:24: public JpaKeysetQuerySupport(KeysetSliceAssembler assembler) {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java:29: public static JpaKeysetQuerySupport standard() {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java:30: return new JpaKeysetQuerySupport(new KeysetSliceAssembler());
|
|
--- JpaRepositoryFragmentSupport
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaRepositoryFragmentSupport.java:24:public abstract class JpaRepositoryFragmentSupport {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaRepositoryFragmentSupport.java:31: protected JpaRepositoryFragmentSupport(EntityManager entityManager) {
|
|
--- JpaStreamExecutor
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaStreamExecutor.java:27:public final class JpaStreamExecutor {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaStreamExecutor.java:41: public JpaStreamExecutor(QueryObservation observation) {
|
|
--- KeysetPredicateBuilder
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/KeysetPredicateBuilder.java:32:public final class KeysetPredicateBuilder {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/KeysetPredicateBuilder.java:34: private KeysetPredicateBuilder() {}
|
|
--- SpecificationPolicy
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/SpecificationPolicy.java:18:public final class SpecificationPolicy {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/SpecificationPolicy.java:23: private SpecificationPolicy() {}
|
|
--- QuerydslJpaSupport
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/querydsl/QuerydslJpaSupport.java:21:public final class QuerydslJpaSupport {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/querydsl/QuerydslJpaSupport.java:28: public QuerydslJpaSupport(JPAQueryFactory queryFactory) {
|
|
=== querydsl lock/declarations ===
|
|
43-com.puppycrawl.tools:checkstyle:13.5.0=checkstyle
|
|
44-com.querydsl:querydsl-core:5.1.0=compileClasspath,jpaPlatformPerformanceTestCompileClasspath,jpaPlatformPerformanceTestRuntimeClasspath,postgresqlIntegrationTestCompileClasspath,postgresqlIntegrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
|
|
45:com.querydsl:querydsl-jpa:5.1.0=compileClasspath,jpaPlatformPerformanceTestCompileClasspath,jpaPlatformPerformanceTestRuntimeClasspath,postgresqlIntegrationTestCompileClasspath,postgresqlIntegrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
|
|
46-com.sun.istack:istack-commons-runtime:4.1.2=jpaPlatformPerformanceTestRuntimeClasspath,postgresqlIntegrationTestRuntimeClasspath,runtimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
|
|
47-com.tngtech.archunit:archunit-junit5-api:1.3.0=jpaPlatformPerformanceTestCompileClasspath,jpaPlatformPerformanceTestRuntimeClasspath,postgresqlIntegrationTestCompileClasspath,postgresqlIntegrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
|
|
69: compileOnly 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
|
|
72: testImplementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
|
|
=== statement inspector configuration ===
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/NamedStatementInspector.java:20:public final class NamedStatementInspector implements StatementInspector {
|
|
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaRepositoryFragmentSupport.java:66: * Runs {@code work} with {@code name} bound as the statement-inspector identity.
|
|
=== export lists ===
|
|
/**
|
|
* The packages a leaf outside this one may import from.
|
|
*
|
|
* <p>An export list, because 318 of 324 production files declare a public top-level type and
|
|
* "public" inside one jar is public to everybody who has the jar. The composition root imported
|
|
* twenty-two notification implementation types by name, which turned each of them into a
|
|
* cross-leaf source contract: moving one to another package, or reducing its visibility, broke
|
|
* the root's compile. The notification wiring is now a facade the leaf owns, and this names what
|
|
* the rest of the repository may reach for.
|
|
*
|
|
* <p>Everything absent from this list is internal. It may still be {@code public} — Java has no
|
|
* finer granularity inside one jar — but importing it from another leaf is a boundary violation
|
|
* this test reports, not a compile error the language provides.
|
|
*/
|
|
private static final Set<String> EXPORTED_PACKAGES =
|
|
Set.of(
|
|
// The contracts other leaves are meant to speak.
|
|
"api",
|
|
// Capability configuration facades, each assembling its own capability's internals.
|
|
"notification.configuration",
|
|
// Cross-cutting seams a composition root legitimately wires.
|
|
"transaction",
|
|
"security",
|
|
"observation",
|
|
"migration",
|
|
"hibernate",
|
|
"fileserver",
|
|
"failure",
|
|
"config");
|
|
|
|
private static final JavaClasses PRODUCTION_CLASSES =
|
|
// Future NoSQL adapter modules (for example adapter-persistence-mongodb) must be added as
|
|
// sibling modules that implement application/domain ports directly. When such a module exists,
|
|
// add an ArchUnit rule forbidding jakarta.persistence.., org.hibernate..,
|
|
// org.springframework.data.jpa.., org.flywaydb.., and
|
|
// dev.caskeleton.adapter.outbound.persistence.postgresql..
|
|
// dependencies from that module.
|
|
|
|
/**
|
|
* The composition root reaches only the persistence leaf's exported surface.
|
|
*
|
|
* <p>318 of that leaf's 324 production files declare a public top-level type, and {@code public}
|
|
* inside one jar is public to everyone holding the jar. The root imported twenty-two notification
|
|
* implementation types by name — every store, every repository, the record mapper — which made
|
|
* each of them a cross-leaf source contract: moving one to another package, or reducing its
|
|
* visibility, broke this module's compile. The notification wiring is a facade the leaf owns now,
|
|
* and this keeps the surface from re-growing one convenient import at a time.
|
|
*
|
|
* <p>The list is the leaf's export allowlist, mirrored here because this is the consumer side of
|
|
* the same boundary. {@code JpaModuleBoundaryTest} owns the definition.
|
|
*/
|
|
@ArchTest
|
|
static final ArchRule BOOTSTRAP_USES_ONLY_THE_PERSISTENCE_EXPORT_SURFACE =
|
|
noClasses()
|
|
.that()
|
|
.resideInAPackage("dev.caskeleton.bootstrap..")
|
|
.should()
|
|
.dependOnClassesThat(
|
|
new com.tngtech.archunit.base.DescribedPredicate<>(
|
|
"a persistence type outside the exported surface") {
|
|
private static final java.util.Set<String> EXPORTED =
|
|
java.util.Set.of(
|
|
"api",
|
|
"notification.configuration",
|
|
"transaction",
|
|
"security",
|
|
"observation",
|
|
"migration",
|
|
"hibernate",
|
|
"fileserver",
|
|
"failure",
|
|
"config",
|
|
// The two vendor entry points. A vendor configuration imports the core JPA
|
|
// config rather than the reverse, so there is no single internal entry the
|
|
// composition root could gate instead — inverting the import to make one
|
|
// produced a package cycle. Admitting exactly these two, and no other
|
|
// vendor
|
|
// internals, is the narrower price.
|
|
"postgresql",
|
|
"h2");
|
|
private static final String ROOT = "dev.caskeleton.adapter.outbound.persistence.";
|
|
|
|
@Override
|
|
public boolean test(com.tngtech.archunit.core.domain.JavaClass candidate) {
|
|
String packageName = candidate.getPackageName();
|
|
if (!packageName.startsWith(ROOT)) {
|
|
return false;
|
|
}
|
|
String remainder = packageName.substring(ROOT.length());
|
|
return EXPORTED.stream()
|
|
.noneMatch(
|
|
exported ->
|
|
remainder.equals(exported) || remainder.startsWith(exported + "."));
|
|
}
|
|
})
|
|
.as(
|
|
"app-bootstrap imports only the persistence leaf's exported packages: every other"
|
|
+ " public type there is internal, and importing one freezes it as a contract.")
|
|
.allowEmptyShould(true);
|
|
|
|
/**
|
|
* A persistence adapter stores what a use case decided; it does not make the decision.
|
|
*
|
|
* <p>{@code JpaNotificationRequestStore} imported {@code NotificationSubmissionService} and
|
|
|
|
Exit code: 0
|