Skip to content
  • Intermediate

Enforce directory structure with the Layout system

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

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

There are a few ways to write a directory declaration. You can use whichever style you prefer.

When each level has only one thing in it, you can chain them with / on a single line.

layout {
"src/main/kotlin/com/example/useCase" / "*UseCase".ktFile()
}

When you want to place multiple things in the same directory, wrap them in a block.

layout {
"src/main/kotlin/com/example/useCase" {
"Get*UseCase".ktFile()
"Update*UseCase".ktFile()
}
}

Inside the block, you can write description. It becomes the explanation of “what this location is for” in the generated documentation.

layout {
"src/main/kotlin/com/example/useCase" {
description = "Used from multiple screens"
"*UseCase".ktFile()
}
}

anyFile() — allow only files directly inside

Section titled “anyFile() — allow only files directly inside”

Allows any file to be placed, but only directly inside that directory.

layout {
"generated" { anyFile() }
}

In this example, subdirectories aren’t allowed. A file one level deeper is still reported as a violation.

ignore() — stop checking everything below

Section titled “ignore() — stop checking everything below”

Stops checking everything below that directory, at any depth.

layout {
"build".ignore()
}

You can also write it inside a block as "build" { ignore() }.

capture(), which names a wildcard in a destination, is used together with templates. For checking layout alone you don’t need capture(); a plain * is fine. See Generating code from a template for how to write it.

Writing out every directory for every Role is inefficient. Extract them into constants at a meaningful unit.

val appPackageDir = "src/main/kotlin/com/example"
architecture {
"UseCase" {
layout {
appPackageDir / "useCase" / "*UseCase".ktFile()
}
}
}
  • layout { } declares where a Role’s files may appear. A file in an undeclared location is a violation.
  • anyFile() and ignore() let you allow files of any kind to be placed, but they are escape hatches from deny by default. Use them with care.
  • capture(), which names a wildcard in a destination, is used together with templates (Generating code from a template). For checking layout alone, a plain * is fine.
val appPackageDir = "src/main/kotlin/com/example" // paths can be pulled out into constants
architecture {
"UseCase" {
layout {
appPackageDir / "useCase" / "*UseCase".ktFile() // join with "/" to declare where files go
appPackageDir { // wrapping in a block declares the same thing
"legacy" / "*UseCase".ktFile()
}
}
}
"GeneratedFile" {
summary = "Files generated by a tool"
layout {
"generated" { anyFile() } // any file directly inside is allowed (no subdirectories)
}
}
"BuildOutput" {
summary = "Build output"
layout {
"build".ignore() // nothing below is checked
}
}
}
  • Role … Learn how to split a definition into files and functions as it grows.
  • Konsist integration … Learn how to constrain not just placement but also the contents of files.
  • Your first architecture definition … Introduce katachi into your project and actually write a declaration.