Konture Architecture Guide
This document describes the internal architecture of Konture, explaining the technical design, Gradle build integrations, and source code static analysis mechanisms.
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
Key Advantages of This Architecture:
- Gradle Configuration Cache Safe: The topology is extracted during Gradle configuration and execution, allowing the task
generateArchitectureLayoutto be fully cached. - 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.
- No Classpath/ClassLoader Leaks: The Kotlin compiler embeddable libraries are kept out of the production classpath, isolated inside the test runner process.
- 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.json)
At the heart of Konture is the layout.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"],
"kotlinFiles": ["io/github/baole/database/Database.kt"]
}
],
"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.json safely, Konture uses Gradle’s Consuming/Publishing Artifacts API:
sequenceDiagram
participant Root as Root Project (:generateArchitectureLayout)
participant Out as Layout Configuration (Outgoing)
participant Cons as Test Module (:konture-test)
Root->>Out: Registers layout.json as an Outgoing Artifact
Cons->>Cons: Automatically sets up consumer layout configuration
Cons->>Out: Resolves artifact via Attribute matching (konture.layout)
Out-->>Cons: Copies layout.json to test/resources/layout.json
The Sharing Mechanism:
- Producer (Root Project):
- Registers the
generateArchitectureLayouttask. - Declares a custom consumable configuration named
kontureLayoutElements. - Associates the configuration with a specific attribute:
ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTEset tokonture-layout-json. - Registers the task output (
layout.json) as an artifact of this configuration.
- Registers the
- Consumer (Test Module):
- When the plugin is applied to a subproject, it automatically sets up a custom resolvable configuration named
archLayoutIncoming. - Declares a dependency on the root project under this configuration.
- Sets the matching attribute to
konture-layout-json. - Registers a copy task that extracts
layout.jsonfrom the resolved configuration and copies it directly into the test project’s generated resources folder before thetesttask runs.
- When the plugin is applied to a subproject, it automatically sets up a custom resolvable configuration named
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:
- Embeddable Environment:
PsiParserinstantiates an IntelliJ core environment in-memory usingKotlinCoreEnvironment.createForProduction. - Virtual Files: The AST loader creates lightweight
KtFilerepresentations of Kotlin source code from local files on disk. - AST Traversal: It traverses the AST using
KtVisitorand extracts:- Package declarations (
packageName) - Class and interface structures (
KtClassOrObject) - Annotations (
KtAnnotationEntry) - Import lists (
KtImportDirective) - Typename references inside class bodies (signatures, property types, generics, local variables)
- Package declarations (
- Disposal: To avoid severe memory leaks across repeated test invocations or build daemon reuse,
PsiParserimplements 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
Aimports the FQN ofB(import com.acme.B), thenAdepends onB. - FQN Reference: If class
AreferencesB’s fully qualified name inside its code, thenAdepends onB. - Simple Name Reference: If
AreferencesB’s simple name (e.g.,val x: B), Konture verifies:- If
AandBreside in the same package (implicitly visible). - If
Ahas a star-import ofB’s package (import com.acme.*). - If
Ahas an explicit import matchingB’s name.
- If
This heuristic-based approach provides 99%+ accuracy for architectural assertions while maintaining absolute independence from compiled classfiles and build-classpath states.