KOTLIN ARCHITECTURE TEST
katachi
Files you didn’t declare
are not allowed to exist
katachi in 5 minutes
00 — INTRO
Sound
familiar?
katachi
02 / 25
00 — INTRO
utils/ becomes a dumping ground
Files dropped there “for now” keep piling up
“Does this really go here?”
Misplaced files easily slip through review
AI agents create files in odd places
The rules are in the docs, yet nobody follows them
Design docs go stale
The documented structure has drifted from the code
katachi
03 / 25
01
CHAPTER 01 — PROBLEM
Why does your
directory structure
turn into a mess
01 — PROBLEM
Docs can’t enforce anything
“Write UseCases like this” is easily broken by newcomers and AI agents
docs/architecture.md
Put UseCases in useCase/
Only one invoke function
…is all it says
Time, people, AI
Actual code
com/example/
├── useCase/
│ └── GetUserUseCase.kt
└── DeleteUserUseCase.kt
Misplaced
katachi
05 / 25
01 — PROBLEM
It always starts with one file
Not a “violation”, but something growing where no one expected
src/main/kotlin/com/example/
├── useCase/
│ └── GetUserUseCase.kt
├── UserHelper.kt
Unexpected
└── misc/
Unexpected
└── memo.md
katachi
06 / 25
01 — PROBLEM
Outside the net, nothing is checked
Konsist and ArchUnit narrow down targets first, then check their contents
src/main/kotlin/com/example/
├── useCase/
│ └── GetUserUseCase.kt
Konsist’s view (*UseCase only)
├── UserHelper.kt
Different name, so it’s never seen
└── misc/memo.md
The whole directory is never seen
katachi
07 / 25
02
CHAPTER 02 — SOLUTION
Allow only
what you declare
02 — SOLUTION
Declare roles and where they live
Describe file kinds (Roles) and their allowed locations in a Kotlin DSL
val projectArchitecture = architecture {
"UseCase" { // Role
layout {
// ⭐️ Only files placed here are allowed to exist
"src/main/kotlin/com/example/useCase" / "*UseCase".ktFile()
}
}
}
katachi
09 / 25
DENY BY DEFAULT
Only declared places
are allowed to exist
Every undeclared file is reported as a violation
katachi
10 / 25
03
CHAPTER 03 — TRY IT
Let’s
try it
03 — TRY IT 1 / 4
Add a dependency, write one test
That’s all the setup. It runs as a regular JUnit test
build.gradle.kts
testImplementation("me.tbsten.katachi:katachi:<version>")
ProjectArchitectureTest.kt
class ProjectArchitectureTest {
@Test
fun `project structure matches the definition`() {
// ⭐️ This one line checks the whole project
projectArchitecture.assert()
}
}
katachi
12 / 25
03 — TRY IT 2 / 4
Start by declaring just UseCase
val projectArchitecture = architecture {
"domain".group {
"UseCase" {
summary = "One app-specific behavior triggered by a screen"
layout {
// ⭐️ Allow only *UseCase.kt in usecase/
"domain/src/main/kotlin/usecase" {
"*UseCase.kt".file()
}
}
}
}
}
katachi
13 / 25
03 — TRY IT 3 / 4
Run it: undeclared files turn red
Each violation comes with nearby valid locations and how to fix it
$ ./gradlew :architecture-test:test
Sample output
Katachi check failed: 1 violation (Unexpected: 1, Missing: 0)
[UnexpectedFile] domain/src/main/kotlin/GetUserUseCase.kt
No role is defined for this file.
Nearby locations:
UseCase domain/src/main/kotlin/usecase/
How to fix:
- Move it to one of the locations above
katachi
14 / 25
03 — TRY IT 4 / 4
Follow the hint, and it turns green
Add, move, or delete until the structure matches the definition
BEFORE — 1 violation
domain/src/main/kotlin/
├── GetUserUseCase.kt
Violation
└── usecase/
Move
AFTER — 0 violations
domain/src/main/kotlin/
└── usecase/
└── GetUserUseCase.kt
OK
Test passed — the structure matches the definition
katachi
15 / 25
04
CHAPTER 04 — DESIGN
Design philosophy
04 — WHY DENY BY DEFAULT
Allow lists fail safe
The same principle as Saltzer & Schroeder’s (1975) fail-safe defaults
DENY LIST
Only blocks what
you thought of
Unexpected → passes silently
ALLOW LIST — katachi
List everything
that may appear
Unexpected → blocked (a violation)
Same idea as a firewall’s default deny or CSP’s default-src 'none'
katachi
17 / 25
04 — ADOPTION
Lots of violations at first is fine
The first report lists the files nobody can explain
STEP 1
See violations
Unexplained
files surface
STEP 2 — Resolve one by one
Add to the definition
Move
Delete
STEP 3
Re-agree
as a team
Revisit the architecture
* Baseline (accepting the current state) is on the roadmap but not implemented yet. For now, get violations via process() and subtract them
katachi
18 / 25
04 — SINGLE SOURCE OF TRUTH
One definition, both tests and docs
summary and description are both checked and become the docs
architecture { }
Roles, locations, descriptions
Tests
Check that the project matches the declaration
Docs
Explain why it’s structured this way (planned for v0.2)
Custom Processor
Aggregate, extract, or export to other formats
katachi
19 / 25
05
CHAPTER 05 — ECOSYSTEM
Other tools &
defining roles
05 — COMPARISON
Only katachi asks “may this file exist?”
Other tools check the contents and relationships of existing code
Tool
Checks
Runs as
Use with
katachi
Whether a file may exist
Unit test
—
Konsist
Class & function contents
Unit test
Recommended
detekt
Static analysis, code smells
Gradle / CLI
Possible
ArchUnit
Class dependencies, layers
Unit test
Possible
ProjectGuard
Module dependencies
Gradle
Possible
Konture
Module & package structure
Unit test
Possible
katachi
21 / 25
05 — WITH KONSIST
Where: katachi. What’s inside: Konsist
Konsist rules in the definition give one report for location and content
"UseCase" {
layout {
"src/main/kotlin/com/example/useCase" / "*UseCase".ktFile()
}
// ⭐️ Content rules live in the same definition
"has exactly one invoke function".konsist {
classes().must { it.functions().map { f -> f.name } == listOf("invoke") }
}
}
katachi
22 / 25
05 — NAMING ROLES
Use the words your team already uses
No new concepts. Turn the words from your reviews into declarations
“This is a UseCase, right?”
“Put it in the Repository”
“Keep screen state in the ViewModel”
“Wrap API calls in a DataSource”
"UseCase" { … }
"Repository" { … }
"ViewModel" { … }
"DataSource" { … }
The definition is a team agreement. An AI-drafted one is only a guess
katachi
23 / 25
06
CHAPTER 06 — SUMMARY
Summary
SUMMARY
Test your
directory structure,
deny by default