Starphix
Software Engineering / Sources
The Lab / Sources / Software Engineering / Languages/JVM-Kotlin-Scala.md

JVM: Kotlin, Scala, Java

Modern patterns, tooling, and best practices for the JVM ecosystem.


Language Comparison

Aspect Kotlin Scala 3 Java 21+
Paradigm Multi-paradigm (OO + FP) Functional-first OO + FP (records, patterns)
Null Safety Built-in (?, !!) Option / Null Annotations (@Nullable)
Data Classes data class case class record
Pattern Matching when + sealed classes match + ADTs switch + patterns (preview)
Coroutines First-class (suspend) ZIO / Cats Effect / Ox Virtual threads (Loom)
Type Inference Local Global (improved in 3) Local (var)
Compilation JVM / JS / Native / WASM JVM / JS / Native JVM
Learning Curve Low (Java-like) Medium-High Low (familiar)

Idiomatic Patterns

// Null safety
fun findUser(id: UserId): User? = repository.findById(id)
val name = user?.name ?: "Unknown"

// Sealed classes for state machines
sealed interface OrderState {
    data class Pending(val createdAt: Instant) : OrderState
    data class Confirmed(val confirmedAt: Instant) : OrderState
    data class Shipped(val tracking: String) : OrderState
    data class Delivered(val deliveredAt: Instant) : OrderState
    data class Cancelled(val reason: String) : OrderState
}

// Extension functions
fun User.fullName() = "$firstName $lastName"

// Scope functions
val user = userRepository.findById(id)
    ?.also { log.info("Found user: ${it.id}") }
    ?: throw NotFoundException(id)

// Result type (Arrow or custom)
sealed class Result<out T, out E> {
    data class Success<out T>(val value: T) : Result<T, Nothing>()
    data class Failure<out E>(val error: E) : Result<Nothing, E>()
}

fun createUser(cmd: CreateUserCmd): Result<User, DomainError> =
    Email(cmd.email).map { User(cmd.name, it) }
        .flatMap { repo.save(it).map { it } }

Coroutines (Structured Concurrency)

// suspend functions = async without callback hell
suspend fun getDashboard(userId: UserId): Dashboard = coroutineScope {
    val (profile, orders, recs) = coroutineScope {
        async { profileService.get(userId) }
        async { orderService.recent(userId) }
        async { recService.forUser(userId) }
    }.awaitAll()
    Dashboard(profile, orders, recs)
}

// Supervision + timeout
withTimeout(5_000) {
    supervisorScope {
        children.forEach { child ->
            launch { child.process() }
        }
    }
}

// Flow (cold async streams)
fun userUpdates(userId: UserId): Flow<User> = callbackFlow {
    val listener = UserListener { offer(it) }
    repo.addListener(userId, listener)
    awaitClose { repo.removeListener(userId, listener) }
}

Spring Boot 3 + Kotlin

// Controller
@RestController
@RequestMapping("/api/v1/users")
class UserController(private val service: UserService) {

    @GetMapping("/{id}")
    fun getUser(@PathVariable id: UserId): ResponseEntity<User> =
        service.getUser(id).fold(
            { ResponseEntity.notFound().build() },
            { ResponseEntity.ok(it) }
        )

    @PostMapping
    fun createUser(@Valid @RequestBody cmd: CreateUserCmd): ResponseEntity<User> =
        ResponseEntity.status(HttpStatus.CREATED).body(service.createUser(cmd))
}

// Configuration
@Configuration
@ConfigurationProperties("app")
data class AppConfig(
    val database: DatabaseConfig,
    val redis: RedisConfig,
    val kafka: KafkaConfig
)

// Virtual threads (Java 21+)
@Bean
fun taskExecutor(): ExecutorService = Executors.newVirtualThreadPerTaskExecutor()

Scala 3 (For FP-Heavy Teams)

Effect Systems

// ZIO 2 (recommended for production)
import zio.*

trait UserRepository {
  def findById(id: UserId): IO[RepoError, Option[User]]
  def save(user: User): IO[RepoError, Unit]
}

case class UserService(repo: UserRepository) {
  def createUser(cmd: CreateUserCmd): ZIO[Any, DomainError, User] =
    for {
      email <- Email.make(cmd.email).mapError(DomainError.InvalidEmail)
      user  = User(cmd.name, email)
      _     <- repo.save(user).mapError(DomainError.RepoError)
    } yield user
}

// Structured concurrency
val dashboard = for {
  fiber1 <- profileService.get(userId).fork
  fiber2 <- orderService.recent(userId).fork
  fiber3 <- recService.forUser(userId).fork
  profile <- fiber1.join
  orders  <- fiber2.join
  recs    <- fiber3.join
} yield Dashboard(profile, orders, recs)

Type-Level Programming

// Phantom types for compile-time state tracking
trait State
trait Draft extends State
trait Published extends State

case class Article[S <: State](title: String, content: String)

def publish(article: Article[Draft]): Article[Published] =
  article.asInstanceOf[Article[Published]] // safe: only called after validation

// GADTs for type-safe DSL
enum Expr[A] {
  case Lit(value: Int) extends Expr[Int]
  case Add(l: Expr[Int], r: Expr[Int]) extends Expr[Int]
  case If[B](cond: Expr[Boolean], t: Expr[B], f: Expr[B]) extends Expr[B]
}

HTTP (http4s + Cats Effect)

object UserRoutes {
  def apply[F[_]: Async](service: UserService[F]): HttpRoutes[F] = HttpRoutes.of {
    case GET -> Root / "users" / UUIDVar(id) =>
      service.getUser(UserId(id)).flatMap {
        case Some(user) => Ok(user.asJson)
        case None => NotFound()
      }
    case req @ POST -> Root / "users" =>
      req.asJsonDecode[CreateUserCmd].flatMap(cmd =>
        service.createUser(cmd).flatMap(Created(_))
      )
  }
}

Java 21+ (Modern Java)

Records + Pattern Matching

// Immutable data carriers
public record UserId(UUID value) {}
public record User(UserId id, String email, String name, Instant createdAt) {}

// Sealed interfaces for exhaustive matching
public sealed interface OrderState permits Pending, Confirmed, Shipped {
    record Pending(Instant createdAt) implements OrderState {}
    record Confirmed(Instant confirmedAt) implements OrderState {}
    record Shipped(String tracking) implements OrderState {}
}

// Pattern matching (preview)
String describe(OrderState state) = switch (state) {
    case Pending(var created) -> "Pending since " + created;
    case Confirmed(var confirmed) -> "Confirmed at " + confirmed;
    case Shipped(var tracking) -> "Shipped: " + tracking;
};

// Virtual threads (Project Loom)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<User>> futures = userIds.stream()
        .map(id -> executor.submit(() -> userService.getUser(id)))
        .toList();
    // ...
}

Spring Boot 3 + Virtual Threads

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public ExecutorService virtualThreadExecutor() {
        return Executors.newVirtualThreadPerTaskExecutor();
    }
}

// Controller uses virtual threads automatically for blocking calls
@RestController
@RequiredArgsConstructor
public class UserController {
    private final UserService service;

    @GetMapping("/users/{id}")
    public ResponseEntity<User> getUser(@PathVariable UserId id) {
        return service.getUser(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
}

Build Tools

// build.gradle.kts
plugins {
    id("org.springframework.boot") version "3.2.0"
    id("io.spring.dependency-management") version "1.1.4"
    kotlin("jvm") version "1.9.0"
    kotlin("plugin.spring") version "1.9.0"
    kotlin("plugin.serialization") version "1.9.0"
}

dependencies {
    implementation(platform("org.springframework.boot:spring-boot-dependencies:3.2.0"))
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
    implementation("io.arrow-kt:arrow-core:1.2.0")

    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
    testImplementation("io.kotest:kotest-assertions-core:5.8.0")
    testImplementation("io.mockk:mockk:1.13.13")
}

tasks.withType<Test> {
    useJUnitPlatform()
    testLogging { events("passed", "skipped", "failed") }
}

Maven (if preferred)

<!-- pom.xml -->
<properties>
    <java.version>21</java.version>
    <kotlin.version>1.9.0</kotlin.version>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>3.2.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Database Access

Spring Data (R2DBC for Reactive)

// Kotlin + R2DBC
interface UserRepository : ReactiveCrudRepository<User, UserId> {
    fun findByEmail(email: String): Mono<User>
}

// Service
@Service
class UserService(private val repo: UserRepository) {
    fun getUser(id: UserId): Mono<User> = repo.findById(id)

    fun createUser(cmd: CreateUserCmd): Mono<User> =
        Email(cmd.email).fold(
            { Mono.error(DomainError.InvalidEmail(it)) },
            { email -> repo.save(User(UserId.random(), email, cmd.name, Instant.now())) }
        )
}

jOOQ (Type-Safe SQL) — Alternative

// Generated code from DSLContext
dsl.insertInto(USERS)
   .set(USERS.ID, UUID.randomUUID())
   .set(USERS.EMAIL, email)
   .set(USERS.NAME, name)
   .execute()

val user = dsl.selectFrom(USERS)
    .where(USERS.ID.eq(id))
    .fetchOptionalInto(User::class.java)

Testing

Kotest (Kotlin)

class UserServiceTest : BehaviorSpec() {
    init {
        Given("a user repository") {
            val repo = mockk<UserRepository>()
            val service = UserService(repo)

            When("creating a user with valid email") {
                val cmd = CreateUserCmd("John", "john@example.com")
                every { repo.save(any()) } returns Result.Success(Unit)

                Then("user is created") {
                    val result = service.createUser(cmd)
                    result.shouldBeSuccess()
                }
            }
        }
    }
}

ScalaTest + ZIO Test

object UserServiceSpec extends ZIOSpecDefault {
  def spec = suite("UserService")(
    test("create user succeeds with valid email") {
      for {
        repo <- ZIO.service[UserRepository]
        service = UserService(repo)
        result <- service.createUser(CreateUserCmd("John", "john@example.com"))
          .exit
      } yield assert(result)(succeeds(anything))
    }
  ).provide(InMemoryUserRepository.layer)
}

JUnit 5 + Mockito (Java)

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock UserRepository repo;
    @InjectMocks UserService service;

    @Test
    void createUser_savesUser() {
        when(repo.save(any())).thenReturn(Mono.empty());

        StepVerifier.create(service.createUser(new CreateUserCmd("John", "john@example.com")))
            .expectNextMatches(u -> u.email().equals("john@example.com"))
            .verifyComplete();
    }
}

Observability

Micrometer + OpenTelemetry (All Languages)

// Spring Boot 3 auto-configures Micrometer + OTel
@Configuration
class ObservabilityConfig {
    @Bean
    fun meterFilter(): MeterFilter =
        MeterFilter.deny(id -> id.getName().startsWith("jvm."))
}
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    tags:
      application: ${spring.application.name}
  otel:
    metrics:
      export:
        interval: 10s
    traces:
      exporter: otlp

Tooling Stack (2024+)

Category Kotlin Scala Java
Build Gradle (Kotlin DSL) Mill / sbt Gradle / Maven
Linting detekt + ktlint scalafix + scalafmt Checkstyle / SpotBugs
Formatting ktlint scalafmt google-java-format
Testing Kotest / JUnit 5 munit / ZIO Test / ScalaTest JUnit 5 + Mockito
Property Testing kotest-property ScalaCheck jqwik
DI Spring / Koin / Kodein ZIO Layer / MacWire Spring / Dagger
Serialization kotlinx.serialization circe / uPickle Jackson
HTTP Client ktor.client / Spring WebClient sttp / http4s client Spring WebClient
Observability Micrometer + OTel OpenTelemetry Java Micrometer + OTel
Vulnerability Scan OWASP Dependency Check OWASP Dependency Check OWASP Dependency Check

When to Choose What

Scenario Recommendation
Team knows Java, wants modern Kotlin + Spring Boot 3
FP-first, complex domain modeling Scala 3 + ZIO / Cats Effect
Enterprise, long-term support, conservative Java 21 LTS + Spring Boot 3
High throughput, low latency Java 21 Virtual Threads / Kotlin Coroutines
Data engineering, Spark Scala 3
Android + Backend shared code Kotlin Multiplatform
Team split on FP vs OO Kotlin (bridges both)

  • Language Agnostic Principles — Universal patterns
  • TypeScript JavaScript — JS/TS patterns
  • Go — Go patterns
  • Rust — Rust patterns
  • Architectural Styles — Hexagonal on JVM
  • Design Patterns — Patterns in JVM languages
  • CI CD Best Practices — Pipeline for JVM
  • Test Pyramid — Testing strategy

Sources: Kotlin Docs, Scala 3 Docs, Java 21 Docs, Spring Boot 3 Reference, ZIO Docs, Cats Effect Docs, Arrow Docs, Kotest, ScalaTest, JUnit 5, Micrometer, OpenTelemetry Java, Gradle, Mill, sbt, detekt, ktlint, scalafix, scalafmt, Error Prone

From the Software Engineering research vault · CC BY 4.0 · all notes · raw markdown