Role
Declaring a Role
Section titled “Declaring a Role”An app that follows an architecture definition has files with specific roles (Roles).
For example UseCase, Repository, Controller, or ViewModel.
You define what Roles exist inside architecture { }.
architecture { // Define a Role called UseCase "UseCase" { // Write the description, placement, and constraints inside here }}You write a Role’s description, placement, and constraints inside that Role’s block.
title,summary,description, etc. … The description of the Role. Used when generating documentation.layout { }… Defines where the files for that Role may be placed.- See Layout definitions for details.
"constraint name".konsist { }… Defines constraints you can apply to that Role’s files.- Constraints are defined using Konsist. See Konsist integration for details.
.template { }… Attached to a file declared inlayout { }, it defines the template used when that file is newly created.- See Generate code from a template for details.
architecture { "UseCase" { title = "Use case" summary = "A single app-specific behavior that occurs on each screen"
example("GetUserUseCase", "Fetch the user")
// Where it may be placed layout { "src/main/kotlin/com/example/useCase" / "*UseCase".ktFile() }
// What its contents should look like "has exactly one invoke function".konsist { classes().must { it.functions().map { f -> f.name } == listOf("invoke") } } }}Let’s look at each of these in a bit more detail.
Writing a description
At minimum, all you need is summary. It becomes the one-line entry for the Role in the generated documentation’s list table.
"UseCase" { summary = "A single app-specific behavior that occurs on each screen"}description can hold a longer explanation, and example() a representative example. Calls to example() are kept in the order you call them.
"UseCase" { title = "Use case" summary = "A single app-specific behavior that occurs on each screen" description = """ Call only UseCase from the UI; never touch Repository directly.
### Things not to do - Depend on Android types """.trimIndent()
example("GetUserUseCase", "Fetch the user") example("UpdateUserUseCase", "Update the user")}Declaring where files go
This is the case where a Role has just one place to live.
"UseCase" { layout { "src/main/kotlin/com/example/useCase" / "*UseCase".ktFile() }}You can write multiple layout { } blocks. This declares that the same kind of file is allowed to appear in more than one place. Files of different kinds go in separate Roles.
"Screen" { // In the feature-home module layout { "feature-home/src/main/kotlin/screen" / "*Screen".ktFile() }
// And in the feature-settings module layout { "feature-settings/src/main/kotlin/screen" / "*Screen".ktFile() }}Placing a file anywhere you haven’t declared is a violation.
src/main/kotlin/com/example/GetUserUseCase.kt ← placed outside useCase/When you run the test, katachi reports the nearby valid location and how to fix it together in a single block.
Katachi check failed: 1 violation (Unexpected: 1)
[UnexpectedFile] file:///path/to/project/src/main/kotlin/com/example/GetUserUseCase.kt No role is defined for this file.
Nearby locations: UseCase file:///path/to/project/src/main/kotlin/com/example/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" / "GetUserUseCase".ktFile() } }Constraining the contents
You give each constraint a name. That name appears directly in the violation report.
"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") } }}A constraint applies to the files that matched the Role’s layout { }. You cannot write a constraint on a Role that has no layout { }.
You can also express a prohibition with mustNot and mustBeEmpty().
"RepositoryInterface" { layout { "src/main/kotlin/com/example/repository" / "*Repository".ktFile() }
// Write the required condition as a negation "must not be internal".konsist { classes().mustNot { it.hasInternalModifier } }
// Require that none match "must not place Impl in the same location".konsist { classes().filter { it.hasNameEndingWith("Impl") }.mustBeEmpty() }}A class that doesn’t satisfy the constraint is a violation.
// Placed inside useCase/, but has a function other than invokeclass GetUserUseCase(private val repository: UserRepository) { suspend fun invoke(id: String) = repository.find(id) fun clearCache() = repository.clear() // ← this one}When the test calls assert(FileConstraintCheck()), it shows up in the same report as placement violations. It even tells you which file and which line.
[UnsatisfiedFileConstraint] file:///path/to/project/src/main/kotlin/com/example/useCase/GetUserUseCase.kt Role: UseCase / FileConstraint: "has exactly one invoke function" Declaration: GetUserUseCase (line 3) Declared at: ProjectArchitecture.kt:12Also, because Role names are used for things like documentation generation, they must be ASCII only (starting with a letter, followed by letters, digits, _, or -). Put a Japanese (or any other) display name in title.
Grouping Roles
Section titled “Grouping Roles”You can group Roles together using "group name".group { ... }.
architecture { "domain".group { "UseCase" { // ... }
"RepositoryInterface" { // ... } }}Best practices for designing Roles
Section titled “Best practices for designing Roles”A good Role definition needs no changes
Section titled “A good Role definition needs no changes”If you have to rewrite architecture { } every time you add a feature to the app, that architecture { } definition is poorly designed.
Make good use of wildcards like * and ** so that architecture { } doesn’t end up as a duplicate of your directory structure, requiring double the work to maintain.
Put only one kind of file in a Role
Section titled “Put only one kind of file in a Role”Make each Role stand for exactly one kind of file.
For example, a Repository interface and its implementation have similar names but are different kinds of file. If you put both in one Role, its description, constraints, and templates split by “which of the two files this is about”, and your konsist { } constraints end up with branches like “if it is the interface…, if it is the implementation…”.
With one Role per kind, the description and constraints only deal with that kind of file, and the generated documentation gets one page per kind.
🆖 Two kinds of file in one Role
"Repository" { summary = "Loads and saves data" layout { "repository" / "*Repository".ktFile() // interface "repository" / "*RepositoryImpl".ktFile() // implementation } "Declarations only for the interface, and for the implementation…".konsist { // the constraint has to branch on which file it is }}🆗 One Role per kind
"RepositoryInterface" { summary = "The entry point for loading and saving data" layout { "repository" / "*Repository".ktFile() } "Put only interfaces here".konsist { classes().mustBeEmpty() }}
"RepositoryImplementation" { summary = "The implementation of RepositoryInterface" layout { "repository" / "*RepositoryImpl".ktFile() }}Splitting Role definitions
Section titled “Splitting Role definitions”The bigger the project, the more the architecture { } definition tends to bloat.
Because architecture { } is a Kotlin DSL, you can factor it out into extension functions just like ordinary Kotlin code.
Here’s the recommended way to split Role definitions.
Splitting the architecture { } definition
Section titled “Splitting the architecture { } definition”What tends to bloat is the detail inside each Role. Factor each Role out into its own extension function.
Before
architecture { "UseCase" { layout { "src/main/kotlin/com/example/project/useCase" / "*UseCase".ktFile() } "has exactly one invoke function".konsist { classes().must { it.functions().map { f -> f.name } == listOf("invoke") } } // ... }
"RepositoryInterface" { layout { "src/main/kotlin/com/example/project/repository" / "*Repository".ktFile() } "must not hold a class".konsist { classes().mustBeEmpty() } // ... }
// ...}After
- ProjectArchitecture.kt
Directorydomain/
- UseCaseRole.kt
- RepositoryInterfaceRole.kt
architecture { useCase() repositoryInterface()
// ...}// hint: DeclarationContainerScope is the receiver of architecture { } and "...".group { }.fun DeclarationContainerScope.useCase() = "UseCase" { layout { "src/main/kotlin/com/example/project/useCase" / "*UseCase".ktFile() } "has exactly one invoke function".konsist { classes().must { it.functions().map { f -> f.name } == listOf("invoke") } } // ...}fun DeclarationContainerScope.repositoryInterface() = "RepositoryInterface" { layout { "src/main/kotlin/com/example/project/repository" / "*Repository".ktFile() } "must not hold a class".konsist { classes().mustBeEmpty() } // ...}Splitting the inside of a Role definition
Section titled “Splitting the inside of a Role definition”Within a Role definition, "...".konsist { } in particular tends to bloat as the number of constraints grows or they get more complex.
Don’t hesitate to factor "...".konsist { } out into private extension functions.
Before
fun DeclarationContainerScope.useCase() = "UseCase" { layout { "src/main/kotlin/com/example/project/useCase" / "*UseCase".ktFile() } "has exactly one invoke function".konsist { classes().must { it.functions().map { f -> f.name } == listOf("invoke") } } // ... }After
fun DeclarationContainerScope.useCase() = "UseCase" { layout { "src/main/kotlin/com/example/project/useCase" / "*UseCase".ktFile() }
onlyInvokeFun() // ... }
private fun RoleScope.onlyInvokeFun() = "has exactly one invoke function".konsist { classes().must { it.functions().map { f -> f.name } == listOf("invoke") } }Summary
Section titled “Summary”- A Role is a kind of file that’s allowed to appear in the project. It’s independent of packages and modules.
- A single Role brings together its description (
title/summary), placement (layout { }), and constraints (konsist { }). - Role definitions bloat easily. To keep
architecture { }and Role definitions from bloating, make good use of extension functions to split them appropriately.
architecture { "domain".group { "UseCase" { summary = "A single app-specific behavior that occurs on each screen"
// However many places it is put in, the description stays in this one place layout { "core/domain/src/main/kotlin/useCase" / "*UseCase".ktFile() }
"has exactly one invoke function".konsist { classes().must { it.functions().map { f -> f.name } == listOf("invoke") } } } }}Next steps
Section titled “Next steps”- Layout … Learn how to declare where a Role’s files may be placed.
- Konsist integration … Learn how to constrain the contents of a Role’s files.
- Your first architecture definition … Introduce katachi into your project and actually write a Role.