Build Tools #

A build tool is the foundation of every serious Kotlin project. It manages how code is compiled, how dependencies are downloaded and managed, how artifacts are packaged, and how tests are run. Choosing the right build tool and understanding how to configure it is the skill that separates developers who can merely “write code” from those who can genuinely manage a project professionally. In the Kotlin ecosystem, Gradle is the primary, highly recommended choice — it’s the official build tool for Android and has the deepest integration with the Kotlin toolchain. This article covers Gradle in depth with Kotlin DSL, Maven as an alternative in enterprise environments, a comparison of available build tools, and best practices for project structure.

Build Tool Map in the Kotlin Ecosystem #

flowchart TD
    A[New Kotlin Project] --> B{Project Type?}
    B -- Android --> C[Gradle + Kotlin DSL\nThe only practical choice]
    B -- Backend / Library / CLI --> D{Team Environment?}
    D -- Already using Maven --> E[Maven + kotlin-maven-plugin]
    D -- New project --> F[Gradle + Kotlin DSL\nRecommended]
    B -- Large Monorepo --> G[Bazel\nFor very large scale]

Gradle — The Primary Build Tool for Kotlin #

Gradle is a build tool based on the Groovy/Kotlin DSL that uses the concepts of incremental build and build cache — it only recompiles the parts that changed, making the build process much faster on large projects. Gradle has two DSL languages: Groovy (old, build.gradle files) and Kotlin DSL (modern, build.gradle.kts files). For Kotlin projects, use the Kotlin DSL — more type-safe, better IDE auto-complete, and consistent with the language you’re writing.

Standard Gradle Project Structure #

kotlin-project/
├── build.gradle.kts          ← main build configuration
├── settings.gradle.kts       ← project name and submodule list
├── gradle.properties         ← global properties
├── gradlew                   ← Gradle wrapper script (Linux/macOS)
├── gradlew.bat               ← Gradle wrapper script (Windows)
├── gradle/
│   ├── wrapper/
│   │   └── gradle-wrapper.properties  ← the Gradle version used
│   └── libs.versions.toml    ← Version Catalog (optional but recommended)
└── src/
    ├── main/
    │   └── kotlin/           ← main source code
    └── test/
        └── kotlin/           ← test code

settings.gradle.kts — Project Starting Point #

// settings.gradle.kts
rootProject.name = "kotlin-app"

// Submodule list for a multi-module project
include(":core")
include(":api")
include(":service")
include(":repository")

build.gradle.kts — Main Build Configuration #

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.0.0"
    application  // for creating a runnable application
}

group = "com.myapp"
version = "1.0.0"

repositories {
    mavenCentral()  // main repository for Java/Kotlin libraries
    maven("https://jitpack.io")  // additional repository
}

dependencies {
    // Main dependencies
    implementation("org.jetbrains.kotlin:kotlin-stdlib")

    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")

    // Logging
    implementation("ch.qos.logback:logback-classic:1.5.3")

    // Test
    testImplementation("org.jetbrains.kotlin:kotlin-test")
    testImplementation("io.mockk:mockk:1.13.10")
}

application {
    mainClass.set("com.myapp.MainKt")  // program entry point
}

kotlin {
    jvmToolchain(17)  // target JDK 17
}

tasks.test {
    useJUnitPlatform()
}

Version Catalog — Centralized Version Management #

The Version Catalog is a modern Gradle feature (since version 7.4) that allows defining all library versions in one place. It eliminates version duplication in multi-module projects and simplifies updates.

gradle/libs.versions.toml #

[versions]
kotlin = "2.0.0"
coroutines = "1.8.0"
ktor = "2.3.9"
exposed = "0.49.0"
logback = "1.5.3"
mockk = "1.13.10"
junit = "5.10.2"

[libraries]
# Kotlin
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }

# Ktor
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" }
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }

# Database
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }

# Logging
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }

# Test
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }

[bundles]
# Library groups always used together
ktor-server = ["ktor-server-core", "ktor-server-netty", "ktor-server-content-negotiation"]
exposed = ["exposed-core", "exposed-dao", "exposed-jdbc"]
testing = ["mockk", "junit-jupiter"]

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

Using the Version Catalog in build.gradle.kts #

plugins {
    alias(libs.plugins.kotlin.jvm)
    alias(libs.plugins.kotlin.serialization)
}

dependencies {
    // Individual libraries
    implementation(libs.kotlinx.coroutines)
    implementation(libs.logback.classic)

    // Bundle — add a group of libraries at once
    implementation(libs.bundles.ktor.server)
    implementation(libs.bundles.exposed)

    // Test
    testImplementation(libs.bundles.testing)
}

Multi-Module Projects with Gradle #

For large projects, splitting code into separate modules provides benefits: faster incremental compilation, clear separation of concerns, and modules that can be reused in other projects.

Multi-Module Structure #

backend-app/
├── settings.gradle.kts
├── build.gradle.kts          ← root configuration (shared config)
├── gradle/
│   └── libs.versions.toml
├── core/                     ← domain model, shared utilities
│   ├── build.gradle.kts
│   └── src/main/kotlin/
├── repository/               ← database access
│   ├── build.gradle.kts
│   └── src/main/kotlin/
├── service/                  ← business logic
│   ├── build.gradle.kts
│   └── src/main/kotlin/
└── api/                      ← HTTP handlers, entry point
    ├── build.gradle.kts
    └── src/main/kotlin/

Root build.gradle.kts — Shared Configuration #

// build.gradle.kts (root)
plugins {
    alias(libs.plugins.kotlin.jvm) apply false  // apply false = don't apply to root
}

// Configuration that applies to all submodules
subprojects {
    apply(plugin = "org.jetbrains.kotlin.jvm")

    repositories {
        mavenCentral()
    }

    kotlin {
        jvmToolchain(17)
    }

    tasks.test {
        useJUnitPlatform()
    }
}

core/build.gradle.kts — Module Without Internal Dependencies #

// core/build.gradle.kts
dependencies {
    implementation(libs.kotlinx.coroutines)
    testImplementation(libs.bundles.testing)
}

service/build.gradle.kts — Module Depending on Other Modules #

// service/build.gradle.kts
dependencies {
    // Dependencies to internal modules
    implementation(project(":core"))
    implementation(project(":repository"))

    // External libraries
    implementation(libs.kotlinx.coroutines)
    testImplementation(libs.bundles.testing)
}

Custom Tasks in Gradle #

Gradle lets you define custom tasks for automation — code generation, packaging, deployment, and more.

// Simple task
tasks.register("greet") {
    group = "custom"
    description = "Display a welcome message"
    doLast {
        println("Hello from a Gradle task!")
    }
}

// Task with inputs and outputs (incremental)
tasks.register<Copy>("copyConfig") {
    group = "custom"
    description = "Copy configuration files to the build dir"
    from("src/main/resources/config")
    into("${buildDir}/config")
    include("*.yaml", "*.properties")
}

// Task that depends on other tasks
tasks.register("buildAndCheck") {
    group = "custom"
    dependsOn("build", "test")
    doLast {
        println("Build and test succeeded!")
    }
}

// Modify an existing task
tasks.named<Jar>("jar") {
    manifest {
        attributes(
            "Main-Class" to "com.myapp.MainKt",
            "Implementation-Version" to project.version
        )
    }
    // Fat JAR — include all dependencies
    from(configurations.runtimeClasspath.get().map {
        if (it.isDirectory) it else zipTree(it)
    })
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

The Most Commonly Used Gradle Commands #

# Build the project
./gradlew build

# Run tests only
./gradlew test

# Run the application
./gradlew run

# Clean previous build results
./gradlew clean

# Build without tests
./gradlew build -x test

# See all available tasks
./gradlew tasks

# Run a task in a specific submodule
./gradlew :service:test

# Display dependencies
./gradlew dependencies
./gradlew :api:dependencies --configuration runtimeClasspath

# Build with profiling (for build performance analysis)
./gradlew build --profile

# Build with full info
./gradlew build --info

# Force re-downloading all dependencies
./gradlew build --refresh-dependencies

gradle.properties — Global Configuration #

# gradle.properties

# Increase JVM memory for large builds
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m

# Enable the build cache
org.gradle.caching=true

# Enable parallel builds (for multi-module)
org.gradle.parallel=true

# Enable the configuration cache (Gradle 8+)
org.gradle.configuration-cache=true

# Kotlin version to use across the project
kotlin.code.style=official

# Android-specific (if an Android project)
android.useAndroidX=true
android.enableJetifier=true

Maven — For Enterprise Environments #

Maven uses XML (pom.xml) for configuration and relies on convention over configuration — the standard directory structure doesn’t need to be configured. Maven is still widely used in large companies that already have Maven infrastructure (Nexus, Artifactory).

Maven Project Structure #

maven-project/
├── pom.xml                   ← main build configuration
└── src/
    ├── main/
    │   └── kotlin/           ← source code
    └── test/
        └── kotlin/           ← test code

A Complete pom.xml for Kotlin #

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.myapp</groupId>
    <artifactId>kotlin-app</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <kotlin.version>2.0.0</kotlin.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- Kotlin stdlib -->
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib</artifactId>
            <version>${kotlin.version}</version>
        </dependency>

        <!-- Coroutines -->
        <dependency>
            <groupId>org.jetbrains.kotlinx</groupId>
            <artifactId>kotlinx-coroutines-core</artifactId>
            <version>1.8.0</version>
        </dependency>

        <!-- Test -->
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-test-junit5</artifactId>
            <version>${kotlin.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
        <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>

        <plugins>
            <!-- Kotlin compilation plugin -->
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <version>${kotlin.version}</version>
                <executions>
                    <execution>
                        <id>compile</id>
                        <phase>compile</phase>
                        <goals><goal>compile</goal></goals>
                    </execution>
                    <execution>
                        <id>test-compile</id>
                        <phase>test-compile</phase>
                        <goals><goal>test-compile</goal></goals>
                    </execution>
                </executions>
                <configuration>
                    <jvmTarget>17</jvmTarget>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Commonly Used Maven Commands #

# Build and install to the local repository
mvn install

# Build without tests
mvn install -DskipTests

# Run tests only
mvn test

# Clean
mvn clean

# Full clean + build
mvn clean install

# Display dependencies
mvn dependency:tree

# Update dependency versions
mvn versions:display-dependency-updates

Build Tool Comparison #

AspectGradle (Kotlin DSL)MavenBazel
Configuration languageKotlin (type-safe)XML (verbose)Starlark (Python-like)
Learning curveModerateLow-ModerateHigh
PerformanceVery good (incremental, cache)ModerateVery good (distributed cache)
Plugin ecosystemVery richVery richLimited
Android support✓ Official✗ No✓ Limited
Multi-module✓ Very good✓ Good✓ Excellent
IDE integration✓ Very good✓ Good✓ Adequate
Best forAll Kotlin projectsEnterprise / Java migrationVery large monorepos

Gradle Wrapper — Locking the Gradle Version #

The Gradle Wrapper (gradlew) ensures all team members use the same Gradle version, without needing to install Gradle manually. Always commit the wrapper files to the repository.

# gradle/wrapper/gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
# Update the Gradle wrapper version
./gradlew wrapper --gradle-version 8.7

# Verify the version in use
./gradlew --version
Always use ./gradlew (not gradle directly) inside a project. This ensures all developers and CI/CD use the same Gradle version as configured in gradle-wrapper.properties, not whatever version happens to be installed on each system.

Tips and Best Practices #

Optimize Build Performance #

// build.gradle.kts

// Enable incremental annotation processing
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
    compilerOptions {
        freeCompilerArgs.addAll(
            "-opt-in=kotlin.RequiresOptIn",
            "-Xjsr305=strict"  // strict null-safety for Java interop
        )
    }
}

// Parallel test configuration
tasks.withType<Test> {
    maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
    forkEvery = 100  // restart the JVM every 100 tests to prevent memory leaks
}

Separate Source Sets for Integration Tests #

// Add a new source set for integration tests
sourceSets {
    create("integrationTest") {
        kotlin.srcDir("src/integrationTest/kotlin")
        resources.srcDir("src/integrationTest/resources")
        compileClasspath += sourceSets["main"].output + configurations["testRuntimeClasspath"]
        runtimeClasspath += output + compileClasspath
    }
}

tasks.register<Test>("integrationTest") {
    description = "Run integration tests"
    group = "verification"
    testClassesDirs = sourceSets["integrationTest"].output.classesDirs
    classpath = sourceSets["integrationTest"].runtimeClasspath
    shouldRunAfter("test")
}

Summary #

  • Gradle + Kotlin DSL is the standard — for new Kotlin projects, always choose Gradle with build.gradle.kts. Type-safe, IDE auto-complete, and consistent with the language you write.
  • Use the Version Catalog — define all versions in gradle/libs.versions.toml. This eliminates version duplication in multi-module projects and makes library updates easier and centralized.
  • The Gradle Wrapper is mandatory — commit gradlew, gradlew.bat, and gradle/wrapper/ to the repository. Use ./gradlew instead of gradle directly to ensure version consistency.
  • Enable org.gradle.caching=true and org.gradle.parallel=true in gradle.properties to significantly speed up builds, especially in multi-module projects.
  • Multi-module projects for large codebases — split into core, repository, service, api modules. This speeds up incremental compilation and makes the dependency graph clearer.
  • Maven for enterprise environments — if the team already has Maven infrastructure (Nexus, CI configured for Maven), use kotlin-maven-plugin. No need to migrate to Gradle if there’s existing Maven investment.
  • Bazel for very large monorepos — only consider Bazel if the project is truly huge (thousands of modules), needs distributed build caches, or has a dedicated DevOps team to manage the build infrastructure.
  • Optimize build performance — enable incremental compilation, parallel test execution, and the configuration cache. For large projects, the build time difference can be significant.

← Previous: Regex   Next: Multithreading →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact