Skip to content
  • Beginner

Motivation and where katachi fits

There’s also a 5-minute slide deck that gives you the big picture of katachi.

Architecture documentation always drifts from the implementation

Section titled “Architecture documentation always drifts from the implementation”

Being the diligent developer you are, your project surely has a /docs directory that spells out the overall structure and the rules for every component.

But the sentence “a UseCase is written like this” in that documentation has no power to enforce anything. A newcomer who does not know the project’s rules, or an AI agent that is uncertain by nature, can break those rules without effort.

Konsist is one of the tools that act as a guardrail against code that ignores project rules like these.

Konsist lets you write your project’s rules as unit tests.

class UseCaseKonsistTest {
@Test
fun `a UseCase has exactly one invoke function`() {
Konsist
.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.assertTrue { it.functions().map { function -> function.name } == listOf("invoke") }
}
}

At first glance Konsist looks like all you need to enforce your project’s rules. In practice, the way Konsist works makes that hard.

Focusing on particular files is not enough. You need a way to check the project as a whole.

katachi looks at the whole project from above and lets you declare which kinds of files may appear in it and where they may be placed.

val projectArchitecture = architecture {
"UseCase" {
layout {
"src/main/kotlin/com/example/project/useCase" / "*UseCase".ktFile()
}
}
}

katachi has you declare two things:

  • The kinds of files that may appear in the project (these are called Roles)
  • Where the files of that Role may appear

Writing this in a declarative DSL lets you test the directory structure of the entire project.

An example this rule catches

Suppose an AI agent places a file named com.example.project.GetUserUseCase:

com/example/project/GetUserUseCase.kt
package com.example.project // ❌ this belongs in com.example.project.useCase
class GetUserUseCase {
suspend fun invoke() = ...
}
  • Directorysrc/main/kotlin/com/example/project/
    • GetUserUseCase.kt ❌ placed somewhere that was never declared
    • DirectoryuseCase/
      • UpdateUserUseCase.kt ⭐️ exactly where the declaration says

Running the unit test that includes katachi reports the error like this:

Katachi check failed: 1 violation (Unexpected: 1)
[UnexpectedFile] file:///path/to/project/src/main/kotlin/com/example/project/GetUserUseCase.kt
No role is defined for this file.
Nearby locations:
UseCase file:///path/to/project/src/main/kotlin/com/example/project/useCase/
How to fix:
- Move it to one of the locations above
- Delete it if it is not needed
- Add a new role for it:
"GetUserUseCase" {
summary = "TODO"
layout {
"src/main/kotlin/com/example/project" / "GetUserUseCase".ktFile()
}
}

katachi can generate a variety of things from your architecture definition.

Generate documentation

From the project structure you defined with katachi, you can have Markdown documentation generated.

Terminal window
$ ./gradlew katachiDocs

Once you point the Gradle plugin at the definition with katachi { architecture = "..." }, the pages are written to build/katachi/docs of the module that holds the definition.

Architecture definition

ProjectArchitecture.kt
val projectArchitecture = architecture {
"UseCase" {
title = "Use case"
summary = "A single app-specific behavior that occurs on a screen"
example("GetUserUseCase", "Gets a user")
example("UpdateUserUseCase", "Updates a user")
layout {
"src/main/kotlin/com/example/project/useCase" {
description = "Ones used from more than one screen"
"*UseCase".ktFile()
}
}
}
// ...
}

Generated Markdown

build/katachi/docs/UseCase.md
[Architecture](README.md)
# Use case
A single app-specific behavior that occurs on a screen
## Placement
| Path | When to use |
|---|---|
| `src/main/kotlin/com/example/project/useCase/*UseCase.kt` | Ones used from more than one screen |
## Examples
- `GetUserUseCase` ... Gets a user
- `UpdateUserUseCase` ... Updates a user

Treat the project structure you defined with katachi as the single source of truth, and generate documentation that both humans and AI agents can read.

See Document generation for the details.

Generate code from templates

By attaching a typical source code template to a file declaration inside the architecture definition, you can generate that file with a single command.

Terminal window
$ ./gradlew katachiTemplate --arg template=UseCase,UseCaseImplementation --arg name=GetUser

Architecture definition

ProjectArchitecture.kt
val projectArchitecture = architecture {
"UseCase" {
layout {
"src/main/kotlin/com/example/project/useCase" /
"${capture("name")}UseCase".ktFile()
.template {
val name = captureValue("name")
"""
package com.example.project.useCase
interface ${name}UseCase {
suspend operator fun invoke()
}
""".trimIndent()
}
}
}
"UseCaseImplementation" {
layout {
"src/main/kotlin/com/example/project/useCase" /
"${capture("name")}UseCaseImpl".ktFile()
.template {
val name = captureValue("name")
"""
package com.example.project.useCase
class ${name}UseCaseImpl : ${name}UseCase {
override suspend fun invoke() {
// Write the behavior here
}
}
""".trimIndent()
}
}
}
// ...
}

Command to generate, and the generated source code

Terminal window
$ ./gradlew katachiTemplate --arg template=UseCase,UseCaseImplementation --arg name=GetUser
[1/3] Processors: template
[2/3] Processing...
[template] Generating 2 files under file:///path/to/project
[template] Wrote file:///path/to/project/src/main/kotlin/com/example/project/useCase/GetUserUseCase.kt
[template] Wrote file:///path/to/project/src/main/kotlin/com/example/project/useCase/GetUserUseCaseImpl.kt
========================================
[3/3] Katachi processor run: 1 succeeded, 0 failed
[OK] template
========================================
src/main/kotlin/com/example/project/useCase/GetUserUseCase.kt
package com.example.project.useCase
interface GetUserUseCase {
suspend operator fun invoke()
}

See Generating code from a template for the details.

katachi works together with Konsist: it verifies the project as a whole, then checks each individual file.

val projectArchitecture = architecture {
"UseCase" {
// where it may be placed
layout {
"src/main/kotlin/com/example/project/useCase" / "*UseCase".ktFile()
}
// what its contents should look like
"has exactly one invoke function".konsist {
classes()
.withNameEndingWith("UseCase")
.must { it.functions().map { function -> function.name } == listOf("invoke") }
}
}
}

When the test calls assert(FileConstraintCheck()), placement violations and content violations line up in one and the same report.

The recommended split is to test the structure of the whole project with katachi, then check the contents of each file with Konsist.

See Konsist integration for the details.

  • Developing with AI calls for a way to make AI agents follow the rules about project structure.
  • katachi gives you a DSL for defining and testing the structure of the whole project, and for generating documentation from it.
  • katachi for the structure of the whole project, Konsist for the contents of each file. They do different jobs, so using both is the recommended way.
  • Your first architecture definition … Add a katachi architecture definition to your own project and see what it does for you.
  • FAQ … Understand the thinking behind katachi through the questions people ask most.
  • Role definitions … Learn about the Role, the basic unit of an architecture definition in katachi.
  • Layout … Learn about Layout, how katachi defines directory structure.