Загрузка
Все статьи

Micronaut vs Spring Boot — лёгкий фреймворк для Kotlin-микросервисов

Сравниваю Micronaut и Spring Boot для Kotlin-микросервисов: стартовое время, потребление памяти, холодный старт и реальный опыт миграции одного из сервисов.

У нас в команде давно витала мысль: «А не переехать ли нам с Spring Boot на что-то полегче?». Стартовое время инстансов росло, memory footprint на 15 микросервисов превышал 32 GB на проде, а холодный старт в Kubernetes при scaling занимал 25–40 секунд. Я взял на себя задачу — оценить Micronaut как альтернативу и провести честный бенчмарк. Вот что вышло.

Архитектура Micronaut: compile-time DI

Главная разница между Micronaut и Spring Boot — подход к dependency injection. Spring использует runtime reflection для создания бинов: он сканирует classpath, читает аннотации и строит контекст при старте приложения. Micronaut же делает всё это на этапе компиляции через annotation processor. Фреймворк генерирует Java-код для DI-контейнера, и в runtime никаких отражений нет.

Последствия: Micronaut стартует быстрее, потребляет меньше памяти и совместим с GraalVM native image. Но есть и цена — compile-time обработка занимает больше времени при сборке, а некоторые библиотеки, которые полагаются на Spring-специфичные механизмы, не работают из коробки.

Бенчмарк: один и тот же REST-эндпоинт

Я реализовал один и тот же сервис на обоих фреймворках: CRUD для заказов с PostgreSQL, Flyway миграциями и Kotlin coroutines. Вот результаты:

Конфигурация: AWS c6i.xlarge (4 vCPU, 8 GB RAM), Java 21, Kotlin 2.0.21

Метрики:
                          Spring Boot 3.4    Micronaut 4.7
Старт (cold):              8.2 сек            1.4 сек
Старт (warm, JVM cached):  2.1 сек            0.6 сек
RSS memory (idle):         320 MB             95 MB
RSS memory (под нагрузкой): 580 MB            210 MB
Heap (max):                512 MB             256 MB
GraalVM native image:      ~130 MB, 0.4 сек   ~42 MB, 0.02 сек
Throughput (req/s):        12 400             15 800
p99 latency:               18 ms              12 ms
Размер JAR:                68 MB              22 MB

Цифры говорят сами за себя. Micronaut выигрывает по всем метрикам, кроме... экосистемы.

Код: один и тот же эндпоинт на обоих фреймворках

Spring Boot:

@RestController
@RequestMapping("/api/v1/orders")
class OrderController(
    private val orderService: OrderService
) {
    @GetMapping
    suspend fun listOrders(
        @RequestParam page: Int = 0,
        @RequestParam size: Int = 20
    ): Page {
        return orderService.findAll(PageRequest.of(page, size))
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    suspend fun createOrder(
        @RequestBody @Valid request: CreateOrderRequest
    ): OrderDto {
        return orderService.create(request)
    }

    @GetMapping("/{id}")
    suspend fun getOrder(@PathVariable id: String): OrderDto {
        return orderService.findById(id)
            ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
    }
}

Micronaut:

@Controller("/api/v1/orders")
class OrderController(
    private val orderService: OrderService
) {
    @Get
    suspend fun listOrders(
        @QueryValue(defaultValue = "0") page: Int,
        @QueryValue(defaultValue = "20") size: Int
    ): Page {
        return orderService.findAll(Pageable.from(page, size))
    }

    @Post
    @Status(HttpStatus.CREATED)
    suspend fun createOrder(@Body @Valid request: CreateOrderRequest): OrderDto {
        return orderService.create(request)
    }

    @Get("/{id}")
    suspend fun getOrder(id: String): OrderDto {
        return orderService.findById(id)
            ?: throw NotFoundException("Order not found")
    }
}

Как видите, код практически идентичен. Разница в аннотациях минимальна — @RestController заменяется на @Controller, @GetMapping на @Get, @RequestBody на @Body. Это хорошо — порог входа низкий.

Боли миграции

Реальность оказалась сложнее бенчмарков. Вот с чем мы столкнулись при миграции реального сервиса (не тестового CRUD, а сервиса с 40+ эндпоинтами):

Первое — Spring Security. В Micronaut используется своя система безопасности, и миграция SecurityFilterChain с кастомными фильтрами заняла две недели. Некоторые специфичные вещи (например, JWT с кастомными claims и conditional access) пришлось переписывать с нуля.

Второе — @Transactional. В Micronaut он работает, но нюансы другие. Нет TransactionTemplate для programmatic-транзакций, propagation behaviour отличается. Пришлось несколько раз отлаживать race conditions в concurrent-сценариях.

Третье — экосистема. Spring Data JPA в Micronaut не работает (есть Micronaut Data, но API другой). Spring Cloud (config, discovery, circuit breaker) — нет прямых аналогов, нужно собирать из разных библиотек. А вот если вы используете чистый JDBC или Exposed — migration прямая.

Сообщество и экосистема

Spring Boot — это индустриальный стандарт. На любой вопрос есть ответ на StackOverflow, документация исчерпывающая, а библиотеки — тысячи. Micronaut растёт, но разрыв заметен. На момент написания статьи для Spring Boot существует ~15 000 библиотек-стартеров, для Micronaut — около 200. Если вам нужна специфичная интеграция, есть шанс, что готового модуля нет.

Наше решение

Мы решили компромиссный путь: новые микросервисы с простой логикой (aggregators, gateways, lightweight workers) пишем на Micronaut. Сервисы со сложной бизнес-логикой и глубокой интеграцией со Spring-экосистемой оставляем на Spring Boot. Один сервис уже мигрировали — это наш API Gateway, который стал потреблять 120 MB вместо 450 MB, а стартовать за 0.8 секунды вместо 5.4. Для CI/CD pipeline это означало ускорение деплоя на 3 минуты.

Micronaut не убийца Spring Boot, и я не пытался доказать обратное. Это инструмент для задач, где каждый мегабайт памяти и каждая секунда старта на счету. Если у вас 5 монолитных сервисов — разницы не почувствуете. Если у вас 50+ микросервисов в Kubernetes с auto-scaling — разница в $5000-8000/месяц на инфраструктуру. И это уже аргумент.

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

Будьте первым, кто оставит комментарий к этой статье

Читайте также

Kotlin: sealed interfaces и when-выражения — паттерн-матчинг как в Rust

Как sealed interfaces из Kotlin 1.5+ вместе с when-выражениями дают мощный паттерн-матчинг, похожий на Rust enum+match, и почему мы перешли на ADT для доменных событий.

11 июня 2026 9 мин

Kotlinx Serialization + Protobuf — быстрая сериализация для микросервисов

Разбираюсь с накладными расходами JSON-сериализации при высоких нагрузках и показываю, как интегрировать Protobuf с kotlinx-serialization в микросервисной архитектуре на Kotlin.

10 июня 2026 10 мин

Kotlin + gRPC: streaming и двусторонняя связь в продакшене

Реальный опыт использования gRPC streaming в продакшене на Kotlin: server-side, client-side и bidirectional стриминг для системы отслеживания статусов заказов в реальном времени.

9 июня 2026 12 мин