How Konture Works

This page explains Konture’s internal design: how it extracts project topology at build time, shares it safely across Gradle modules, and parses Kotlin source without a full compiler pass. It’s meant for readers evaluating build-tool safety (Configuration Cache, Project Isolation) or contributing to Konture itself. If you just want to know what Konture checks, see Why Architecture Testing?.


High-Level Architecture

Konture separates project topology extraction from assertion testing. This “offline contract” model decouples the build-time Gradle environment from the test-time JVM environment.

graph TD
    subgraph Build_Time["Build Time (Gradle Plugin)"]
        root[Root Project] -->|generateArchitectureLayout| json[layout.json]
        json -->|Gradle Configuration sharing| sub["Test Module (Layout Consumed Automatically)"]
    end

    subgraph Test_Time["Test Time (Unit Tests)"]
        sub -->|Loads layout.json| pg[ProjectGraphLoader]
        pg -->|Initializes| pg_obj[ProjectGraph]
        pg_obj -->|Spawns| psi[PsiParser]
        psi -->|Parses Kotlin Files via embeddable PSI| ast[AST / ClassDeclarations]
        ast -->|Asserted by| test["Konsist / ArchUnit DSL Styles<br>(JUnit 4/5/6, Kotest, TestBalloon, etc.)"]
    end

Layout extraction happens once at build time; tests only read the cached JSON and parse source files on demand.

Key Advantages of This Architecture:

  1. Gradle Configuration Cache Safe: The topology is extracted during Gradle configuration and execution, allowing the task generateArchitectureLayout to be fully cached.
  2. Project Isolation Compatible: By sharing files via custom configurations and Gradle attributes rather than direct parent-to-child model access, Konture is fully compatible with Gradle’s new Project Isolation requirement. The Konture settings plugin (KontureSettingsPlugin) applies Konture via settings.gradle.lifecycle.beforeProject hooks to every subproject safely without cross-project access.
  3. No Classpath/ClassLoader Leaks: The Kotlin compiler embeddable libraries are kept out of the production classpath, isolated inside the test runner process.
  4. Execution Speed: Because the architecture tests run as ordinary, test-framework agnostic unit tests (loading pre-analyzed metadata and parsing source files on-demand without full Kotlin compiler compilation or codegen), they are extremely fast.

1. The Offline Layout Contract (layout_v2.json)

At the heart of Konture is the layout_v2.json file. It acts as the schema contract between the Gradle plugin and the test runner.

The structure of LayoutModel (defined in core) is serialized using kotlinx-serialization:

{
  "schemaVersion": 1,
  "builds": [
    {
      "id": ":",
      "modules": [
        {
          "path": ":core:database",
          "projectDir": "/Users/user/project/core/database",
          "appliedPlugins": ["kotlin-jvm"],
          "sourceSets": [
            {
              "name": "main",
              "kind": "KOTLIN_JVM",
              "production": true,
              "srcDirs": ["src/main/kotlin"]
            }
          ],
          "dependencies": [
            {
              "configuration": "implementation",
              "targetBuildId": ":",
              "targetPath": ":shared"
            }
          ]
        }
      ]
    }
  ]
}
  • schemaVersion: Used to verify compatibility. If a developer upgrades the test library but forgets to upgrade the Gradle plugin, a mismatch is thrown at runtime rather than failing with silent, hard-to-debug deserialization errors.
  • builds: Supports composite/included builds, mapping multiple separate Gradle builds inside a single layout.

2. Gradle Artifact & Sharing Architecture

Gradle prevents projects from accessing other projects’ internal task and file configurations directly (e.g., parent.subprojects is deprecated and violates Project Isolation).

To share the generated layout_v2.json safely, Konture uses Gradle’s Consuming/Publishing Artifacts API:

sequenceDiagram
    participant Root as Root Project (:generateArchitectureLayout)
    participant Out as Layout Configuration (archLayoutElements)
    participant Cons as Test Module (:konture-test)

    Root->>Out: Registers layout_v2.json as an Outgoing Artifact
    Cons->>Cons: Automatically sets up consumer layout configuration
    Cons->>Out: Resolves artifact via Attribute matching (koarch-layout)
    Out-->>Cons: Copies layout_v2.json to build/resources/test/konture/layout_v2.json

The root project publishes layout_v2.json as a Gradle artifact, and test modules consume it through attribute-matched configurations.

The Sharing Mechanism:

  1. Producer (Root Project):
    • Registers the generateArchitectureLayout task (and generateDependencyGraph when direct external dependency assertions are present).
    • Declares custom consumable configurations named archLayoutElements and archDepsElements.
    • Associates configurations with Usage attributes set to koarch-layout and koarch-deps.
    • Registers task outputs (layout_v2.json and dependencies.json) as artifacts of these configurations.
  2. Consumer (Test Module):
    • When the plugin is applied to subprojects via settings.gradle.kts, setupConsumerLayout sets up resolvable configurations named archLayoutIncoming and archDepsIncoming.
    • Declares dependencies on the root project (:) under these configurations with matching koarch-layout and koarch-deps usage attributes.
    • Registers copy tasks (copyArchitectureLayout and copyDependencyGraph) that copy the resolved layout and dependency graph directly into build/resources/test/konture/ before tests run.

This pure artifact-sharing model allows Gradle to establish a secure task dependency: calling ./gradlew :konture-test:test automatically triggers the root project’s :generateArchitectureLayout first.


3. AST-Level Kotlin PSI Parser Strategy

When writing assertions about classes (e.g. interfaces, annotations, dependencies), Konture must analyze Kotlin source code. To do this without running a full compilation (which is slow and requires extensive classpath setup), Konture uses a standalone IntelliJ PSI (Program Structure Interface) environment.

The component responsible is PsiParser inside library.

Under the Hood:

  1. Embeddable Environment: PsiParser instantiates an IntelliJ core environment in-memory using KotlinCoreEnvironment.createForProduction.
  2. Virtual Files: The AST loader creates lightweight KtFile representations of Kotlin source files from local files on disk.
  3. AST Traversal: It traverses the AST using KtVisitor and extracts:
    • Package declarations (packageName)
    • Class and interface structures (KtClassOrObject)
    • Annotations (KtAnnotationEntry)
    • Import lists (KtImportDirective)
    • Type-name references inside class bodies (signatures, property types, generics, local variables)
  4. Disposal: To avoid severe memory leaks across repeated test invocations or build daemon reuse, PsiParser implements a strict cleanup routine using a disposable parent context (Disposer.dispose).

4. Code-Level Class Dependency Heuristics

Traditional tools like ArchUnit run classloader-level bytecode analysis, which requires loading all compiled classfiles. Since Konture operates directly on source files, it uses a Static Source-Level Reference Heuristic to resolve code-level dependencies:

For any source class A and target class B:

  • Direct Import: If class A imports the FQN of B (import com.acme.B), then A depends on B.
  • FQN Reference: If class A references B’s fully qualified name inside its code, then A depends on B.
  • Simple Name Reference: If A references B’s simple name (e.g., val x: B), Konture verifies:
    1. If A and B reside in the same package (implicitly visible).
    2. If A has a star-import of B’s package (import com.acme.*).
    3. If A has an explicit import matching B’s name.

This heuristic-based approach provides 99%+ accuracy for architectural assertions while maintaining absolute independence from compiled classfiles and build-classpath states. The known boundary is ambiguous simple-name resolution: a dependency can be misattributed when two classes share the same simple name across different packages and the source file does not use an explicit import, star import, same-package reference, or fully qualified name to disambiguate it. In practice this is rare, since Kotlin style conventions and import organization make ambiguous simple-name references uncommon, but it is the trade-off that lets Konture remain source-only and classpath-independent.

These design choices, offline topology extraction, artifact-based sharing, and source-level parsing, let Konture run as an ordinary, fast unit test rather than a slow, classpath-dependent static analysis pass. See Why Architecture Testing? for what these tests actually check.


5. Violation Message Format

When a rule fails, every violation follows a uniform shape so it can be read at a glance and clicked through in an IDE, regardless of whether the subject is a class, file, function, or property:

<Subject> <fully-qualified name> <what was expected>[, but <actual>] (at <module>, <source set> source set, <file>:<line>)

For example:

Function architecture violation(s) detected:
  - Function com.acme.web.UserController.handle should be public, but is INTERNAL (at :web, main source set, src/main/kotlin/com/acme/web/UserController.kt:42)
Total: 1 violation(s)
  • Fully-qualified name identifies the subject unambiguously even when the same simple name appears in several packages or classes.
  • Actual value is shown alongside the expectation where it is cheap to compute (for example, the actual visibility), so the mismatch is visible without opening the file.
  • Location names the module and source set as well as the file and line, which matters in multi-module and multiplatform projects; module rules embed the module path in the message itself. Composite either/or rules separate the sub-conditions of each branch with ; .

All message text is localized; see Configuration for selecting a language. Because baselines match on this text, changing it requires a one-time regeneration — see Architecture Baselines.


6. Structured Violation Engine Model

In addition to throwing formatted localized AssertionError text for unit test runners, Konture’s internal evaluation pipeline constructs structured, serializable violation objects under io.github.baole.konture.core.model:

  • ViolationReport: Top-level container representing rule results (ruleId, violations: List<Violation>, severity).
  • Violation: Atomic violation record containing:
    • ruleId: Unique string ID (e.g. classes.rule).
    • subject: Domain subject element (ModuleSubject, ClassSubject, FunctionSubject, or CustomSubject).
    • target: Optional target subject involved in relationship rules (such as forbidden dependency edges).
    • sourceLocation: Precise source coordinates (filePath, line, column).
    • dependencyPath: List of intermediate subjects for transitive cycle/dependency paths.
    • message: Human-readable violation message.
    • severity: Violation severity rating (INFO, WARNING, ERROR).

Because all core violation models are annotated with @Serializable (kotlinx.serialization), they seamlessly serialize to JSON for IDE plugins, custom test reporters, or external CI quality dashboards.


This site uses Just the Docs, a documentation theme for Jekyll.