Skip to content
  • Beginner

Basic API

This page introduces katachi’s core API. Once you know these, you should understand the basics of using katachi.

architecture { } is the entry point for defining your project’s overall architecture. Inside it, you write the roles of the files that exist in your project.

architecture {
"UseCase" { /* ... */ }
"RepositoryInterface" { /* ... */ }
}
// This project has two roles: `UseCase` and `RepositoryInterface`

The elements inside an architecture (Role) are covered in more depth in a later section.

assert() is the function that tests the architecture defined with architecture { }.

val projectArchitecture = architecture {
"UseCase" { /* ... */ }
"RepositoryInterface" { /* ... */ }
}
@Test
fun `files are placed according to the architecture`() {
projectArchitecture.assert()
}

"Name" { } declares the role that a file appearing in the project plays.

architecture {
"UseCase" { /* ... */ }
"RepositoryInterface" { /* ... */ }
}

Writing "Name" { } inside an architecture { } block lets you declare the roles of files that appear in the project.

If you’re using a well-known architecture, roles typically look something like this.

Architecture Role
MVC Controller
View
Model
MVVM View
ViewModel
Clean architecture Service
UseCase
Entity
RepositoryInterface, RepositoryImplementation

In katachi, only files with a declared Role are allowed to exist in the project.

In other words, adopting katachi requires you to enumerate the Roles that match your project’s conventions.

For Role, also see Role definitions in depth.

"Name".group { } is a block for grouping Roles together.

For example, in Clean architecture, UseCase and RepositoryInterface are both roles in the same domain layer.

architecture {
"domain".group {
"UseCase" { /* ... */ }
"RepositoryInterface" { /* ... */ }
}
}

Expressing a group like “the domain layer” in the DSL makes the relationships and hierarchy between roles clear. Grouping Roles at a sensible granularity is important for building an architecture definition that’s easy to understand.

For grouping Roles, also see Role definitions in depth.

5. Defining where files may exist with layout { }

Section titled “5. Defining where files may exist with layout { }”

Declaring a Role with "..." { } alone doesn’t decide where that Role’s files may be placed.

layout { } is the block that defines where the files for a Role may be placed.

architecture {
"UseCase" {
layout {
"src/main/kotlin/com/example/useCase" / "*UseCase".ktFile()
}
}
}
// A UseCase may be placed at `src/main/kotlin/com/example/useCase/*UseCase.kt`

Under the Deny by default principle, you can’t place a file anywhere that isn’t declared in layout { }.

Say an AI Agent tries to place a new UseCase file at src/main/kotlin/com/example/GetItemUseCase.kt (it should really go inside the useCase package). The moment assert() is called, katachi detects that no Role matches this path, and the test fails.

  • Directorysrc/main/kotlin/com/example/
    • GetItemUseCase.kt ❌ a place not declared in layout { }
    • DirectoryuseCase/
      • GetUserUseCase.kt ⭐️ where the declaration puts it

For layout { }, also see Layout definitions in depth.

6. Constraining a file’s contents with "...".konsist { }

Section titled “6. Constraining a file’s contents with "...".konsist { }”

You can set constraints on the files of a declared Role using Konsist.

architecture {
"UseCase" {
layout {
"src/main/kotlin/com/example/useCase" / "*UseCase".ktFile()
}
"has exactly one invoke function".konsist {
classes().must { it.functions().map { f -> f.name } == listOf("invoke") }
}
}
}

The constraints apply to the files specified in layout { }, and the constraints themselves are written with the Konsist API.

To check the constraints, call assert(FileConstraintCheck()) instead of assert() in the test. With assert() alone the constraints are not evaluated, and the test fails with [UncheckedFileConstraint].

For "...".konsist { }, also see the Konsist integration guide.

APIWhat it doesExample
architecture { }

The function that serves as the entry point for defining your project’s overall architecture.

architecture {
"UseCase" { /* ... */ }
}
  • Meaning: this project has a role called UseCase
assert()

Checks whether files are placed according to the definition. It’s a single line called from a test — without it, nothing happens.

@Test
fun `placement follows the definition`() {
projectArchitecture.assert()
}
  • Meaning: the test fails if there’s a violation. A single run collects every violation and puts them in one message
"Name" { }

Declares the role (Role) of a file that appears in the project.

"UseCase" {
layout { /* ... */ }
}
  • Meaning: there’s a role called UseCase. Files for undeclared roles can’t be placed
"Name".group { }

Groups Roles together to build a hierarchy. Optional — Roles work fine without it.

"domain".group {
"UseCase" { /* ... */ }
}
  • Meaning: UseCase is a role in the domain layer. Its qualified name becomes domain.UseCase
layout { }

Defines where that Role’s files may be placed.

layout {
"domain/useCase" /
"*UseCase".ktFile()
}
  • Meaning: it can only be placed at domain/useCase/*UseCase.kt. Anywhere else is a violation
"Name".konsist { }

Constrains the contents of that Role’s files. Requires the separate katachi-konsist module and assert(FileConstraintCheck()) in the test.

"has exactly one invoke".konsist {
classes().must { /* condition */ }
}
  • Meaning: a file that doesn’t satisfy this is a violation. The constraint name appears directly in the violation report
  • Your first architecture definition … Introduce katachi into your project and actually write these six things.
  • Role … Learn how to write descriptions for roles, and how to keep the definition from becoming a copy of your directory structure.
  • Layout … Learn how to write out placements, and when to reach for the deny-by-default escape hatches anyFile() / ignore().