Skip to content

Dev Environment & First Project

You know TypeScript. You know Go. Now you need Kotlin running on your machine with a real project structure — not an Android app, a backend/CLI Kotlin project. This module gets you from zero to a working Kotlin Gradle project with Docker infrastructure, mapping every tool to its npm/go equivalent as we go.

JDK 21 is the current LTS (Long-Term Support) release. Kotlin compiles to JVM bytecode and runs on the JDK. The mental model:

TypeScript Go Kotlin
Node.js runtime Go runtime (compiled in) JDK (JVM runtime)
nvm manages versions Go manages itself sdkman manages versions
V8 engine Go compiler JVM (HotSpot)

If you use nvm for Node.js versions, SDKMAN is the exact same idea for JDK versions.

  1. Install SDKMAN:

    Terminal window
    curl -s "https://get.sdkman.io" | bash
    source "$HOME/.sdkman/bin/sdkman-init.sh"
    sdk version
  2. Install Eclipse Temurin JDK 21 (the community-standard build):

    Terminal window
    sdk list java # browse available distributions
    sdk install java 21.0.5-tem
    java -version # openjdk version "21.0.5" ...
    javac -version # javac 21.0.5
  3. Set it as your default (like nvm alias default):

    Terminal window
    sdk use java 21.0.5-tem # switch for this shell (like nvm use)
    sdk default java 21.0.5-tem # set the default version

SDKMAN sets JAVA_HOME for you. Verify:

/home/youruser/.sdkman/candidates/java/current
echo $JAVA_HOME
which java
# /home/youruser/.sdkman/candidates/java/current/bin/java

Gradle is the build tool for Kotlin projects. The mental model:

npm (TypeScript) go (Go) Gradle (Kotlin)
package.json go.mod build.gradle.kts
package-lock.json go.sum gradle.lockfile (optional)
node_modules/ $GOPATH/pkg/mod/ ~/.gradle/caches/
npm install go mod download ./gradlew build
npm run build go build ./gradlew build
npm run start go run . ./gradlew run
npm test go test ./... ./gradlew test
npx go run pkg@version ./gradlew (wrapper)
Terminal window
sdk install gradle 8.12
gradle --version

IntelliJ is made by JetBrains — the same company that created Kotlin — so the Kotlin support is unmatched. The Community Edition is free and open source.

Terminal window
sdk install idea
# Or download "Community Edition" from:
# https://www.jetbrains.com/idea/download/

Key settings to configure:

  1. Set JDK: File → Project Structure → Project → SDK → select JDK 21.
  2. Enable auto-import: Settings → Build → Gradle → “Auto-import”.
  3. Kotlin code style: Settings → Editor → Code Style → Kotlin (defaults are good).

Shortcuts you’ll use constantly:

Action macOS Linux
Run current file Ctrl+Shift+R Ctrl+Shift+F10
Navigate to class Cmd+O Ctrl+N
Navigate to file Cmd+Shift+O Ctrl+Shift+N
Find usages Alt+F7 Alt+F7
Refactor/rename Shift+F6 Shift+F6
Quick fix Alt+Enter Alt+Enter
Go to definition Cmd+B Ctrl+B

Initialize a Kotlin application project:

Terminal window
mkdir -p ~/kotlin-hello && cd ~/kotlin-hello
gradle init \
--type kotlin-application \
--dsl kotlin \
--project-name kotlin-hello \
--package com.example \
--no-split-project \
--java-version 21

This generates the following structure:

  • Directorykotlin-hello/
    • Directoryapp/
      • build.gradle.kts deps & build config (like package.json)
      • Directorysrc/
        • Directorymain/kotlin/com/example/
          • App.kt entry point
        • Directorymain/resources/ config files
        • Directorytest/kotlin/com/example/
          • AppTest.kt
    • Directorygradle/
      • libs.versions.toml version catalog
      • Directorywrapper/ wrapper JAR + properties
    • gradlew wrapper script
    • gradlew.bat
    • settings.gradle.kts like the workspace root

The same “where does code live” question, answered in each ecosystem:

  • Directorymy-app/
    • package.json
    • package-lock.json
    • tsconfig.json
    • Directorysrc/
      • index.ts
    • Directorytest/
      • index.test.ts
    • Directorynode_modules/
src/index.ts
console.log("Hello, TypeScript!");
// Run: npx ts-node src/index.ts

Key differences:

  • Kotlin’s main() is a top-level function — no class wrapper needed (unlike Java).
  • println() is a top-level function, not System.out.println().
  • The package declaration maps to the directory structure (like Go, unlike TypeScript).
  • Semicolons are optional and conventionally omitted (like Go).
Terminal window
./gradlew build # compile + run tests
./gradlew run # run the app
./gradlew test # tests only
./gradlew clean # remove build artifacts
./gradlew build -x test # build, skip tests (like npm run build --ignore-scripts)

gradle init creates a multi-module structure by default. For learning, here’s a minimal single-module project you can set up by hand:

settings.gradle.kts
rootProject.name = "kotlin-hello"
build.gradle.kts
plugins {
kotlin("jvm") version "2.1.0"
application
}
group = "com.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
testImplementation(kotlin("test"))
}
tasks.test {
useJUnitPlatform()
}
application {
mainClass.set("com.example.MainKt")
}
src/main/kotlin/com/example/Main.kt
package com.example
fun main() {
println("Hello from Kotlin!")
// String templates (like JS template literals)
val name = "Kotlin Developer"
println("Welcome, $name!")
// Multi-line strings (like JS backticks, Go raw strings)
val message = """
|This is a multi-line string.
|The | character marks the margin.
|Kotlin trims the left margin with trimMargin().
""".trimMargin()
println(message)
}

If you built the project by hand, generate the wrapper so ./gradlew works:

Terminal window
gradle wrapper --gradle-version 8.12
./gradlew run

Here’s an annotated build.gradle.kts mapped to the package.json concepts you already know:

build.gradle.kts
// ===== PLUGINS =====
// Like "devDependencies" that add build capabilities
plugins {
// Compiles .kt files to JVM bytecode (like "typescript" + tsconfig.json)
kotlin("jvm") version "2.1.0"
// Adds the `run` task and packaging (like "scripts": { "start": ... })
application
}
// ===== PROJECT METADATA =====
group = "com.example" // like an npm org scope: @example/my-app
version = "1.0-SNAPSHOT" // SNAPSHOT = development (not released)
// ===== REPOSITORIES =====
// Where to download dependencies (npm has npmjs.com, JVM has Maven Central)
repositories {
mavenCentral()
}
// ===== DEPENDENCIES =====
dependencies {
// Runtime dependency (like npm "dependencies"). Format: "group:artifact:version"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
// Test dependency (like npm "devDependencies")
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
}
tasks.test {
useJUnitPlatform() // use JUnit 5 as the test engine
}
application {
mainClass.set("com.example.MainKt") // the entry point
}
npm (package.json) Go (go.mod) Gradle When available
dependencies require implementation Compile + runtime
dependencies require api Compile + runtime + exposed to consumers
devDependencies require (test) testImplementation Test only
runtimeOnly Runtime only (not at compile time)
compileOnly Compile only (not packaged)

In npm you install lodash; in Go you import github.com/user/repo. On the JVM, dependencies have three parts:

org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0
│ │ │
├── Group ID ├── Artifact ID └── Version
(like an npm scope) (like a package name)

Find them on Maven Central search — the npmjs.com of the JVM world.

Modern Gradle projects centralize dependency versions in a TOML file (like a shared package.json in a monorepo):

gradle/libs.versions.toml
[versions]
kotlin = "2.1.0"
ktor = "3.0.3"
junit = "5.11.3"
[libraries]
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlin" }
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

Then in build.gradle.kts you reference them type-safely:

dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.server.core)
testImplementation(libs.junit.jupiter)
}

This is the project-root config. In a single-module project it’s one line; in a multi-module project it lists the modules — the equivalent of npm workspaces or a Go workspace:

// package.json (npm workspaces)
{
"workspaces": ["app", "core", "api-client"]
}
What you want npm go Gradle
Install dependencies npm install go mod download ./gradlew build (auto-downloads)
Run the app npm start go run . ./gradlew run
Run tests npm test go test ./... ./gradlew test
Build for production npm run build go build -o app ./gradlew build
Clean build output rm -rf dist/ go clean ./gradlew clean
Add a dependency npm install lodash go get github.com/... edit build.gradle.kts, then ./gradlew build
Format code prettier --write . gofmt -w . ./gradlew ktlintFormat (with plugin)
Lint code eslint . golangci-lint run ./gradlew ktlintCheck (with plugin)
List tasks npm run ./gradlew tasks
Run with args npm start -- --port 8080 go run . --port 8080 ./gradlew run --args="--port 8080"

Some flags worth knowing:

Terminal window
./gradlew tasks # list available tasks
./gradlew build --info # more output (like npm --verbose)
./gradlew build -x test # skip a task
./gradlew build --refresh-dependencies # like rm -rf node_modules && npm install
./gradlew run --continuous # rebuild on change (like nodemon / air)
./gradlew dependencies # dependency tree (like npm ls)

This guide uses a shared Docker Compose stack for PostgreSQL, Redis, and Kafka.

  1. Start all services:

    Terminal window
    cd shared-infra
    docker compose up -d
  2. Check status:

    Terminal window
    docker compose ps
    # kotlin-course-postgres running 0.0.0.0:5432->5432/tcp
    # kotlin-course-redis running 0.0.0.0:6379->6379/tcp
    # kotlin-course-kafka running 0.0.0.0:29092->29092/tcp
    # kotlin-course-kafka-ui running 0.0.0.0:8090->8080/tcp
  3. Verify each service:

    Terminal window
    docker exec kotlin-course-postgres pg_isready -U dev
    docker exec kotlin-course-redis redis-cli ping # PONG
    # Kafka UI is available at http://localhost:8090
Service Host Port Credentials
PostgreSQL localhost 5432 user dev, password dev, db kotlin_course
Redis localhost 6379 no auth
Kafka localhost 29092 no auth
Kafka UI localhost 8090 no auth (web UI)

Managing the stack:

Terminal window
docker compose stop # stop, keep data
docker compose down # remove containers, keep volumes
docker compose down -v # remove EVERYTHING, including data
docker compose logs -f postgres # tail a service's logs
docker compose restart redis # restart a single service

Docker for TS/Go devs — what’s different

Section titled “Docker for TS/Go devs — what’s different”

You already use Docker. Here’s the JVM-flavored mental model:

Concept Node.js / Go JVM / Kotlin
Base image node:21-alpine / golang:1.22 eclipse-temurin:21-jre-alpine
Build step npm run build / go build ./gradlew build
Runtime size ~150MB (Node) / ~10MB (Go) ~200MB (JRE) or ~30MB (GraalVM native)
Hot reload nodemon / air ./gradlew run --continuous or Spring DevTools

A production multi-stage Dockerfile for Kotlin:

Dockerfile
# Build stage
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
COPY gradle gradle
COPY gradlew build.gradle.kts settings.gradle.kts ./
RUN ./gradlew dependencies --no-daemon
COPY src src
RUN ./gradlew build -x test --no-daemon
# Runtime stage
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

In TypeScript you import from file paths; in Go, by module path. In Kotlin/JVM you use reverse-domain packages that map to directories:

TypeScript Go Kotlin
import { foo } from './utils' import "myapp/internal/utils" import com.example.myapp.utils.foo
File-path based Module-path based Package based (maps to directories)
src/utils/index.ts internal/utils/utils.go src/main/kotlin/com/example/myapp/utils/Foo.kt
src/main/kotlin/com/example/myapp/models/User.kt
package com.example.myapp.models
data class User(
val id: Long,
val name: String,
val email: String,
)

A typical backend service layout:

  • Directorymy-kotlin-service/
    • build.gradle.kts
    • settings.gradle.kts
    • gradle/libs.versions.toml
    • Directorysrc/
      • Directorymain/kotlin/com/example/myservice/
        • Application.kt entry point
        • Directoryconfig/
        • Directoryroutes/ HTTP handlers (like controllers)
        • Directorymodels/ data classes (like TS interfaces)
        • Directoryservices/ business logic
        • Directoryrepositories/ data access
      • Directorymain/resources/
        • application.conf config (like .env)
        • logback.xml logging config
      • Directorytest/kotlin/com/example/myservice/
    • docker-compose.yml service-specific infrastructure

Kotlin Gradle projects organize code into source sets:

Source set Directory Purpose npm equivalent
main src/main/kotlin/ Application code src/
main resources src/main/resources/ Config, templates config/, .env
test src/test/kotlin/ Test code __tests__/, *.test.ts
test resources src/test/resources/ Test fixtures __fixtures__/

Put the toolchain to work — build a real, small project from scratch.

Terminal window
# Development workflow
sdk use java 21.0.5-tem # ensure the correct JDK
./gradlew build # compile + test
./gradlew run # run the app
./gradlew test # tests only
./gradlew tasks # list available tasks
./gradlew dependencies # show the dependency tree
# Infrastructure
cd shared-infra
docker compose up -d # start Postgres, Redis, Kafka
docker compose ps # check status
docker compose down -v # clean shutdown
# Project init
gradle init --type kotlin-application --dsl kotlin
gradle wrapper --gradle-version 8.12