Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5f4c8ac
chore: ignore prompts for AI
Arc-E-Tect Jul 18, 2026
28092f4
fix(hexagonal-spring-rules): fail fast on missing architecture config…
Arc-E-Tect Jul 18, 2026
35caf8f
feat(hexagonal-spring-rules): add JPA and web DTO leakage rules
Arc-E-Tect Jul 18, 2026
b2ec7be
feat(hexagonal-spring-rules): add package cycle detection rules
Arc-E-Tect Jul 18, 2026
30bc099
test(hexagonal-spring-rules): add self-test fixtures for rule-pack co…
Arc-E-Tect Jul 18, 2026
6cbfac0
feat(hexagonal-spring-rules): add per-rule opt-out configuration
Arc-E-Tect Jul 18, 2026
45f85d0
feat(hexagonal-spring-rules): add no-field-injection architecture rule
Arc-E-Tect Jul 18, 2026
8d5aef9
feat(hexagonal-spring-rules): add opt-in naming convention architectu…
Arc-E-Tect Jul 18, 2026
70919c4
docs(hexagonal-spring-rules): move misplaced DomainIsolationTest Javadoc
Arc-E-Tect Jul 18, 2026
0fe1d2a
chore(gitignore): broaden prompts file ignore pattern
Arc-E-Tect Jul 18, 2026
a804dc9
fix(hexagonal-spring-rules): detect nested adapter cycles reliably
Arc-E-Tect Jul 18, 2026
634d854
refactor(single-module-spring): align fixture with inbound-port conve…
Arc-E-Tect Jul 18, 2026
decb7f4
fix(hexagonal-spring-rules): narrow service rule to adapter dependencies
Arc-E-Tect Jul 18, 2026
46d12e9
feat(architecture-examples): add six spring rule-pack example projects
Arc-E-Tect Jul 18, 2026
54fbfa5
fix(hexagonal-spring-rules): honor split inbound/outbound adapter pro…
Arc-E-Tect Jul 18, 2026
3092d29
test(hexagonal-spring-rules): add regressions for service/adapter rul…
Arc-E-Tect Jul 18, 2026
cedbc59
docs(architecture-validator): deepen rule-example README guidance
Arc-E-Tect Jul 18, 2026
4288c8f
docs(jacoco-marker): explain exclusion convention checks with examples
Arc-E-Tect Jul 18, 2026
66cfcdb
fix(gitignore): unignore application/port/out fixture packages
Arc-E-Tect Jul 18, 2026
d55e83e
refactor(hexagonal-spring-rules): rename test fixture port.in/port.ou…
Arc-E-Tect Jul 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ build/
# IntelliJ IDEA
*.iml
*.iws
out/
/out/

# VS Code
/worktrees/
Expand All @@ -55,3 +55,4 @@ out/
gradle-plugin-publishing.md
sedr-library-maven-central-publishing.md

/**/*prompts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
= Hexagonal Spring Cycle Detection Example
:toc: left

This example demonstrates package cycle detection in adapter packages via `CycleFreedomTest`.
It also documents the `CycleFreedomTest` slice-pattern bug fix that was required to make this check
effective in realistically nested base packages — see "Why this example exists" below.

== Files and Purpose

[cols="1,2"]
|===
|File |Purpose

|`src/main/java/.../cycledetection/adapter/persistence/OrderRepositoryAdapter.java`
|Adapter package A. Depends on `adapter.notification.OrderNotifier`.

|`src/main/java/.../cycledetection/adapter/notification/OrderNotifier.java`
|Adapter package B. Depends back on `OrderRepositoryAdapter`, closing the cycle.

|`src/main/java/.../cycledetection/application/service/OrderService.java`
|Plain application service that depends only on an outbound port — kept compliant so the only failure is the
cycle itself.
|===

== Configuration

[source,groovy]
----
architectureValidator {
basePackage = 'com.arc_e_tect.example.spring.cycledetection'
useBuiltInHexagonalRulePack = false
}
----

No special properties are needed — `CycleFreedomTest` runs against the plugin's default `adapters` pattern
(`..adapter..`/`..adapters..`).

== Why this example exists

`CycleFreedomTest` builds an ArchUnit slice pattern from the configured `adapters` package root. The
original implementation stripped a leading `..` from that pattern (e.g. `..adapters..` became `adapters`),
which anchors the resulting pattern to the *start* of a class's fully-qualified name. For any realistic,
multi-segment base package like `com.arc_e_tect.example.spring.cycledetection`, no class's FQN actually
starts with `adapters.` — so the check matched zero classes and passed vacuously, regardless of real cycles.
This example's base package is deliberately nested several segments deep so it would have silently passed
under the old, buggy pattern; it now correctly fails.

== Run

[source,bash]
----
./gradlew build
./gradlew testArchitecture
----

== Expected violations

The two adapter classes depend on each other, forming a two-package cycle:

[source,java]
----
// adapter.persistence.OrderRepositoryAdapter
public OrderRepositoryAdapter(OrderNotifier notifier) { ... } // depends on adapter.notification

// adapter.notification.OrderNotifier
public OrderNotifier(OrderRepositoryAdapter repositoryAdapter) { ... } // depends back on adapter.persistence
----

`CycleFreedomTest.adapterPackagesShouldBeFreeOfCycles` fails and reports the cycle between
`adapter.persistence` and `adapter.notification`. Every other rule class passes for this fixture.

== Reports

The Gradle HTML report is under `build/reports/architecture-validator/html/`.
The XML report is under `build/reports/architecture-validator/xml/`.

== Fix

Break the dependency loop so `adapter.notification` no longer depends on `adapter.persistence` — for
example, have `OrderRepositoryAdapter` publish through a port or event mechanism instead of calling into
`OrderNotifier` directly, or move the shared collaboration point into a package neither adapter owns. Keep
adapter package dependencies acyclic to preserve maintainable boundaries.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
plugins {
id 'jacoco'
id 'java'
alias(libs.plugins.architecture.validator.iff)
}

group = 'com.arc-e-tect.example.spring'
version = '0.0.1'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}

repositories {
mavenLocal()
mavenCentral()
}

dependencies {
implementation libs.spring.context.iff
testArchitectureImplementation libs.architecture.validator.hexagonal.spring.rules.iff
}

architectureValidator {
basePackage = 'com.arc_e_tect.example.spring.cycledetection'
useBuiltInHexagonalRulePack = false

}

tasks.named('jacocoTestReport') {
reports {
xml.required = true
html.required = true
}
}

tasks.named('testArchitecture', Test) {
ignoreFailures = false
}

tasks.named('check') {
dependsOn tasks.named('testArchitecture')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## Generated by $ ./gradlew refreshVersionsCatalog

[plugins]

architecture-validator-iff = { id = "com.arc-e-tect.architecture-validator", version.ref = "architecture-validator-iff" }

[versions]

architecture-validator-iff = "0.0.1"
architecture-validator-hexagonal-spring-rules-iff = "1.0.0-PRERELEASE"
spring-context-iff = "7.0.8"

[libraries]

architecture-validator-hexagonal-spring-rules-iff = { module = "com.arc-e-tect.architecture-validator:architecture-validator-hexagonal-spring-rules", version.ref = "architecture-validator-hexagonal-spring-rules-iff" }
spring-context-iff = { module = "org.springframework:spring-context", version.ref = "spring-context-iff" }
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading
Loading