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:

TypeScriptGoKotlin
Node.js runtimeGo runtime (compiled in)JDK (JVM runtime)
nvm manages versionsGo manages itselfsdkman manages versions
V8 engineGo compilerJVM (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.jsongo.modbuild.gradle.kts
package-lock.jsongo.sumgradle.lockfile (optional)
node_modules/$GOPATH/pkg/mod/~/.gradle/caches/
npm installgo mod download./gradlew build
npm run buildgo build./gradlew build
npm run startgo run ../gradlew run
npm testgo test ./..../gradlew test
npxgo 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:

ActionmacOSLinux
Run current fileCtrl+Shift+RCtrl+Shift+F10
Navigate to classCmd+OCtrl+N
Navigate to fileCmd+Shift+OCtrl+Shift+N
Find usagesAlt+F7Alt+F7
Refactor/renameShift+F6Shift+F6
Quick fixAlt+EnterAlt+Enter
Go to definitionCmd+BCtrl+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)GradleWhen available
dependenciesrequireimplementationCompile + runtime
dependenciesrequireapiCompile + runtime + exposed to consumers
devDependenciesrequire (test)testImplementationTest only
runtimeOnlyRuntime only (not at compile time)
compileOnlyCompile 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 wantnpmgoGradle
Install dependenciesnpm installgo mod download./gradlew build (auto-downloads)
Run the appnpm startgo run ../gradlew run
Run testsnpm testgo test ./..../gradlew test
Build for productionnpm run buildgo build -o app./gradlew build
Clean build outputrm -rf dist/go clean./gradlew clean
Add a dependencynpm install lodashgo get github.com/...edit build.gradle.kts, then ./gradlew build
Format codeprettier --write .gofmt -w ../gradlew ktlintFormat (with plugin)
Lint codeeslint .golangci-lint run./gradlew ktlintCheck (with plugin)
List tasksnpm run./gradlew tasks
Run with argsnpm start -- --port 8080go 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 course 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
ServiceHostPortCredentials
PostgreSQLlocalhost5432user dev, password dev, db kotlin_course
Redislocalhost6379no auth
Kafkalocalhost29092no auth
Kafka UIlocalhost8090no 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:

ConceptNode.js / GoJVM / Kotlin
Base imagenode:21-alpine / golang:1.22eclipse-temurin:21-jre-alpine
Build stepnpm run build / go build./gradlew build
Runtime size~150MB (Node) / ~10MB (Go)~200MB (JRE) or ~30MB (GraalVM native)
Hot reloadnodemon / 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:

TypeScriptGoKotlin
import { foo } from './utils'import "myapp/internal/utils"import com.example.myapp.utils.foo
File-path basedModule-path basedPackage based (maps to directories)
src/utils/index.tsinternal/utils/utils.gosrc/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 setDirectoryPurposenpm equivalent
mainsrc/main/kotlin/Application codesrc/
main resourcessrc/main/resources/Config, templatesconfig/, .env
testsrc/test/kotlin/Test code__tests__/, *.test.ts
test resourcessrc/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