Микросервисы на Kotlin: архитектура и практика от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему Kotlin — идеальный язык для микросервисов

Современная разработка бэкенда всё чаще отходит от монолитных решений в пользу микросервисной архитектуры. Kotlin, будучи статически типизированным языком, работающим поверх JVM, предлагает разработчикам уникальное сочетание лаконичности, безопасности и производительности. В отличие от Java, Kotlin позволяет писать более выразительный и безопасный код без шаблонного бойлерплейта, что критически важно при построении сложных распределённых систем.

В этой статье мы разберём полный цикл создания микросервиса на Kotlin: от выбора фреймворка до развёртывания в контейнере. Вы узнаете, как использовать Ktor для HTTP-сервера, Koin для внедрения зависимостей и Exposed для работы с базой данных. Все примеры кода готовы к использованию в реальных проектах.

1. Проектирование архитектуры микросервиса

Первый шаг — определить границы сервиса. В отличие от монолита, каждый микросервис отвечает за одну бизнес-возможность. Рассмотрим пример сервиса управления пользователями (User Service).

Структура проекта

Рекомендуемая структура для Kotlin-микросервиса:

user-service/
├── src/
│   ├── main/kotlin/com/example/userservice/
│   │   ├── application/       # Слой приложения (use cases)
│   │   ├── domain/            # Доменные модели
│   │   ├── infrastructure/    # Репозитории, БД, внешние API
│   │   └── presentation/      # Контроллеры (Ktor routes)
│   └── resources/
│       └── application.conf
└── build.gradle.kts

Такая архитектура следует принципам Clean Architecture и Domain-Driven Design, что облегчает тестирование и поддержку.

Выбор технологического стека

  • HTTP-фреймворк: Ktor (лёгкий, асинхронный, корутины)
  • DI: Koin (простой, без рефлексии)
  • ORM: Exposed (Kotlin-native DSL для SQL)
  • База данных: PostgreSQL
  • Контейнеризация: Docker + Docker Compose

2. Реализация микросервиса на Kotlin с Ktor и Koin

Настройка build.gradle.kts

Подключаем необходимые зависимости:

plugins {
    kotlin("jvm") version "1.9.22"
    id("io.ktor.plugin") version "2.3.7"
    kotlin("plugin.serialization") version "1.9.22"
}

dependencies {
    implementation("io.ktor:ktor-server-core:2.3.7")
    implementation("io.ktor:ktor-server-netty:2.3.7")
    implementation("io.ktor:ktor-server-content-negotiation:2.3.7")
    implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.7")
    implementation("org.jetbrains.exposed:exposed-core:0.44.1")
    implementation("org.jetbrains.exposed:exposed-dao:0.44.1")
    implementation("org.jetbrains.exposed:exposed-jdbc:0.44.1")
    implementation("org.postgresql:postgresql:42.6.0")
    implementation("io.insert-koin:koin-ktor:3.5.0")
}

Создание доменной модели и репозитория

// domain/User.kt
data class User(
    val id: Long,
    val name: String,
    val email: String
)

// infrastructure/UserRepository.kt
interface UserRepository {
    suspend fun getAll(): List<User>
    suspend fun getById(id: Long): User?
    suspend fun create(user: User): User
}

Реализация репозитория с Exposed

// infrastructure/UserRepositoryImpl.kt
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction

object UsersTable : Table("users") {
    val id = long("id").autoIncrement()
    val name = varchar("name", 255)
    val email = varchar("email", 255)
    override val primaryKey = PrimaryKey(id)
}

class UserRepositoryImpl(private val database: Database) : UserRepository {
    override suspend fun getAll(): List<User> = dbQuery {
        UsersTable.selectAll().map { it.toUser() }
    }

    override suspend fun getById(id: Long): User? = dbQuery {
        UsersTable.select { UsersTable.id eq id }
            .singleOrNull()?.toUser()
    }

    override suspend fun create(user: User): User = dbQuery {
        UsersTable.insert {
            it[name] = user.name
            it[email] = user.email
        }.resultedValues?.single()?.toUser() ?: throw Exception("Insert failed")
    }

    private fun ResultRow.toUser() = User(
        id = this[UsersTable.id],
        name = this[UsersTable.name],
        email = this[UsersTable.email]
    )
}

// Вспомогательная функция для корутин
suspend fun <T> dbQuery(block: () -> T): T =
    withContext(Dispatchers.IO) { transaction { block() } }

Настройка Koin и Ktor

// application/Application.kt
fun Application.module() {
    install(ContentNegotiation) { json() }
    
    // Подключение Koin
    val database = Database.connect(
        url = "jdbc:postgresql://localhost:5432/userdb",
        driver = "org.postgresql.Driver",
        user = "user",
        password = "pass"
    )
    
    startKoin {
        modules(module {
            single<Database> { database }
            single<UserRepository> { UserRepositoryImpl(get()) }
        })
    }
    
    // Роутинг
    routing {
        get("/users") {
            val repo: UserRepository by inject()
            val users = repo.getAll()
            call.respond(users)
        }
        get("/users/{id}") {
            val id = call.parameters["id"]?.toLongOrNull()
            if (id == null) {
                call.respond(HttpStatusCode.BadRequest, "Invalid ID")
                return@get
            }
            val repo: UserRepository by inject()
            val user = repo.getById(id)
            if (user != null) call.respond(user)
            else call.respond(HttpStatusCode.NotFound)
        }
    }
}

3. Тестирование и контейнеризация

Юнит-тестирование с Koin Test

// test/UserServiceTest.kt
class UserServiceTest : BehaviorSpec({
    val testModule = module {
        single<UserRepository> { MockUserRepository() }
    }
    
    beforeTest {
        startKoin { modules(testModule)                

Блоги

Рекомендаций по книгам