Kotlin Gradle Kotlin DSL Project Setup

Build a small JVM application with named source folders, explicit toolchain and dependencies, then diagnose setup failures by their layer.

Lesson 38 of 43Intermediate9 min read

A Kotlin source file describes application behavior. A Gradle build describes how to compile it, which libraries to obtain, how to run tests, and how to start the application. Keeping these responsibilities separate makes a project reproducible instead of depending on whichever settings happen to exist in one developer's IDE.

The functions lesson supplies the language needed for the application below. This reference explains the build around that code. The example targets Kotlin/JVM, uses the Kotlin DSL file extension .gradle.kts, and pins an educational version combination rather than claiming to show the latest releases.

Identify the files and their responsibilities

Settings.gradle.kts names the project and can declare included subprojects. Build.gradle.kts applies plugins and configures the current project's tasks and dependencies. Source files normally live under src/main/kotlin, and tests under src/test/kotlin. The directory convention lets the Kotlin JVM plugin discover sources without custom source-set code.

The kotlin("jvm") plugin adds Kotlin compilation to the build. The application plugin adds tasks for running and distributing a JVM application. Repositories tell Gradle where dependency artifacts may be resolved; they do not themselves add libraries. Dependencies name the libraries and the scopes in which they are needed.

Kotlin DSL scripts look like Kotlin, but their receivers and available functions come from Gradle and its plugins. A compiler error in build.gradle.kts concerns the build's configuration code, not necessarily your application's Kotlin. Read the filename and line in the error before changing the wrong layer.

Separate versions and scopes

The Kotlin plugin version chooses compiler tooling. Gradle's wrapper version chooses the build engine. The Java toolchain chooses the JDK used for relevant compilation and execution tasks. A library dependency has its own version. These numbers are related through compatibility rules but are not supposed to be identical.

Implementation dependencies are available to main application code and its runtime. TestImplementation dependencies serve test code. A missing test import may mean the library was added in the wrong scope or the IDE has not refreshed the build. A library placed only in testImplementation is not available to src/main/kotlin.

Prefer explicit stable versions or a reviewed version catalog over dynamic versions that can change without a source edit. Dependency locking and verification can strengthen larger builds, but begin by understanding the requested artifact coordinate. Do not fix resolution problems by adding random repositories: each repository is another source of executable build inputs.

Use the wrapper and diagnose one layer at a time

The wrapper scripts and accompanying configuration select the project's Gradle distribution. Commit the normal wrapper files so other developers and automated builds can use the same engine. On a fresh machine, the wrapper may need network access to download that distribution and dependencies. Offline execution works only when the necessary artifacts are already cached.

An unknown plugin points toward plugin resolution or an incorrect plugin identifier. An unresolved application import can point toward the dependency declaration. An unavailable toolchain points toward JDK provisioning. A main-class failure points toward the application entry point. These failures deserve different fixes.

After editing the build, reload or sync the project in your IDE. The command-line build remains useful evidence because it does not depend on an IDE's temporary model. Use the wrapper from the project directory, inspect the first meaningful error, and avoid deleting caches as an automatic first response to every problem.

Official references

Consult Kotlin Gradle configuration for supported tooling combinations, the Gradle Kotlin DSL primer for build-script behavior, and the wrapper guide for reproducible execution. Treat upgrades as an explicit change with compatibility review and a completed command-line build.

Kotlin Gradle Kotlin DSL Project Setup Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A complete small JVM application

Use Kotlin plugin 2.2.20, Gradle 8.14.3 and a Java 17 toolchain for this pinned example. Install or provision JDK 17 first; a toolchain declaration selects a requirement and does not by itself promise automatic downloading on every machine.

With a Gradle 8.14.3 bootstrap installation, generate a wrapper using gradle wrapper --gradle-version 8.14.3. Run ./gradlew run on macOS or Linux, or .\gradlew.bat run on Windows. Gradle also prints task and build messages; the application itself prints:

MainKt is the JVM class generated for this top-level main function in Main.kt without a package declaration. If you add package shop, update the entry point to shop.MainKt. Names and file placement must agree.

Kotlin
rootProject.name = "stock-demo"
Kotlin
plugins {
    kotlin("jvm") version "2.2.20"
    application
}
repositories { mavenCentral() }
kotlin { jvmToolchain(17) }
dependencies { testImplementation(kotlin("test")) }
application { mainClass.set("MainKt") }
tasks.test { useJUnitPlatform() }
Kotlin
fun main() {
    val stock = listOf(2, 4, 3)
    println("Stock total: ${stock.sum()}")
}
Output
Stock total: 9

Kotlin Gradle Kotlin DSL Project Setup in Real Projects

Worked exercise: Add configuration-driven output

Make a greeting accept an optional name while preserving a sensible default. Keep input handling in the application source and keep the build focused on how to run it. The existing build files remain valid; replace Main.kt with this complete program.

Kotlin
fun greeting(arguments: Array<String>): String {
    val name = arguments.firstOrNull()?.trim()?.takeIf { it.isNotEmpty() }
        ?: "learner"
    return "Hello, $name"
}

fun main(args: Array<String>) {
    println(greeting(args))
}

Run the wrapper's run task with --args="Mina". The program's output is:

Output
Hello, Mina

Without --args, it prints Hello, learner. This exercise demonstrates why command-line values belong to the program's inputs, rather than being hard-coded into the build script. Add tests for no argument, whitespace and a normal name using the existing test source set before expanding the behavior.

Kotlin Gradle Kotlin DSL Project Setup FAQ

Are Kotlin DSL and Groovy DSL interchangeable text?

No. They configure the same Gradle model but use different languages and syntax. A snippet from build.gradle may need adaptation before it works in build.gradle.kts. Check which DSL a documentation example targets and avoid combining fragments blindly. Kotlin DSL benefits from typed accessors and IDE assistance, while the plugins still determine the available model. Understand the configuration's purpose before translating its surface syntax.

Does jvmToolchain automatically install any missing JDK?

Not in every project. It declares the required toolchain, and Gradle can use an existing matching installation. Automatic provisioning requires the appropriate configured resolver and environment support. If no matching installation or provisioning mechanism is available, the build can fail. Install JDK 17 for this example or deliberately configure toolchain provisioning. Do not confuse the JDK running Gradle with every JDK a build may request for tasks.

Why use the wrapper when Gradle is already installed?

The wrapper records the engine version intended for the project, so a teammate's global Gradle installation does not silently choose different build behavior. The initial wrapper generation needs a bootstrap, but normal work uses the checked-in scripts afterward. Keep the wrapper configuration and ordinary support files together. Review version upgrades and distribution integrity rather than treating the wrapper as unrelated generated clutter that can safely be discarded.

Why is MainKt the entry point rather than Main?

A top-level Kotlin main function compiles into a JVM file-facade class derived from the source filename. Main.kt normally produces MainKt, and a package declaration adds its qualified package prefix. An object or class-based entry point has a different shape and may require a JVM static main method. Configure the actual compiled entry point rather than guessing from a human-readable project name; packaging and naming conventions directly affect startup.

Kotlin Gradle Kotlin DSL Project Setup Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

Main.kt now declares package shop. Which entry point matches its top-level main?

Kotlin Gradle Kotlin DSL Project Setup Quiz

2 questions to check what you learned.

  1. Which declaration adds a library only for tests?

    • testImplementation(...)
    • application { mainClass.set(...) }
    • repositories { mavenCentral() }
  2. What selects the project's Gradle engine for routine commands?

    • The quantity of dependencies.
    • The checked-in wrapper configuration and scripts.
    • The Kotlin source filename.