Где-то ± раз в год возникает желание посмотреть: если сейчас начинать приложение с нуля, то что бы я обязательно в него включил? Кроме очевидной практической ценности (обычно начинается новый проект) это так же позволяет осознать какой архитектурный опыт получен за последнее время.

Необходимость шаблона сверх https://start.spring.io вызвана следующими типами доработок:

  1. Выбор технологий. Да, всегда пытаешься выбрать как-то осознанно,
    но, в целом, многие варианты подойдут. Тем не менее нужно что-то выбрать. Как из набора start.spring.io, так и дополнительно. Тут технологии как базовые, так и для улучшения опыта разработки (вспомогательные).
  2. Необходимо связать все компоненты вместе. Именно этому и посвящен Spring Boot и start.spring.io. Тем не менее, из кода будет видно, что этого недостаточно и нужно еще писать свои связки.
  3. Лучшие значения по умолчанию: например, непонятно в какой вселенной по умолчанию нужно моментально завершаться, а не постепенно завершать обработку запросов. Это одно из самых ярких, но параметров довольно много изменено.
  4. Примеры: хорошо сразу видеть базовые примеры использования технологий. Можно практически сразу из реального приложения их удалить, тем не менее они останутся в репозитарии шаблона. Это набор лучших практик показывает как обычно нужно кодировать тот или иной случай.

Разницу между https://start.spring.io и тем, что получилось после ручной доработки можно легко отследить по истории комитов.

Шаблон еще хорош тем, что позволяет достаточно легко ответить на вопрос “а что если применить технологию ХХХ?” Например, “а что если для GraphQL использовать библиотеку DGS?” Шаблон небольшой и публичный, но при этом есть код, который реализует типичные эндпоинты – можно поэкспериментировать не отвлекаясь на неважные в эксперименте детали.

Так что, если есть предложения по улучшению – fork & commit. Покажи свое кунг-фу :).

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

Язык: Kotlin

Варианты: Java, Kotlin, Rails, Python, Golang, Node.js

Котлин показывает лучший баланс между легкостью и читаемостью кода и быстротой и стабильностью платформы. В блоге есть несколько статей более детально посещенных этой теме.

Фреймворк: Spring Boot

Варианты: Spring Boot, Quarkus.

Quarkus дает лучший опыт использования для разработчиков (быстрая сборка, тематические руководства, легкая поддержка нативной сборки).

При этом, к сожалению, Quarkus отстает в поддержке ассинхроности в общем и для Котлина (корутины) в частности (например, в GraphQL). И ключевые новшества (http-клиенты через интерфейсы и нативные бинарники) Spring Boot перенял.

С учетом большей популярности среди разработчиков Spring Boot выглядит хорошим вариантом.

Стиль: асинхронные Kotlin co-routines

Варианты: синхронный, асинхронный Reactor, асинхронный Kotlin co-routines

Сейчас нет смысла начинать приложения на синхронном стиле, т.к. асинхронный обладает достаточно очевидными преимуществами и уже хорошо поддерживается языками и фреймворками. Корутины намного читабельнее стиля Rx, поэтому их и используем.

БД: Postgres

Варианты: Postgres, MySQL, Oracle, MongoDB

Postgres как решение по умолчанию. Детальней в одной из заметок на блоге.

SQL API: Spring Data и jOOQ

Варианты: Hibernate (aka JPA), MyBatis, EclipseLink , querydsl, JetBrains Exposed, Panache, jOOQ, Spring Data R2DBC

В общем и целом, все варианты плохи. Возможно, тема слишком сложная.

jOOQ выглядит хорошо, но есть ощущение, что ресурсов или альтернативного пути развития не хватает – сложен для простых случаев.

Для простых запросов хорошо подходит Spring Data.

Подробнее в блоге в специализированных заметках “Выбор ORM для Котлина” и“Особенности jOOQ”.

Миграции БД: в приложении SQL-файлы через Flyway

Варианты: в приложении, внешнее; Flyway, Liquibase

Если исходить из принципов CICD, то миграции должны быть внутри приложения, чтобы применяться автоматически.

Flyway хорош поддержкой sql-файлов. Отмены принципиально не нужны – миграции идут только вперед. Если для разработки нужно откатиться, то можно пересоздать соответствующую базу с нуля.

Система сборки: Gradle Kotlin

Варианты: Maven, Gradle, Gradle с Kotlin

На самом деле не принципиально, но в Gradle попроще добавить кастомный функционал, который иногда нужен.

Если уж мы используем Котлин, то почему бы и в Gradle его не использовать (чтобы только ради Gradle не использовать/учить другой язык).

Система контроля версий: Git

Варианты: CVS, SVN, Git, Perforce, Mercurial, ClearCase, Team Foundation.

Обычно всех устраивает Git. В деталях можно придираться и, надеюсь, когда-нибудь улучшат его (особенно в способах подключать один репозитарий в другой), но ради этого менять саму систему нет смысла.

Если сейчас кто-то использует что-то другое, то это “исторически сложилось” и менять слишком дорого.

UI системы контроля версий: Gitea

Варианты: Microsoft GitHub, Gitlab, Gitea, Atlassian Bitbucket, JetBrains Space

Серьезно выбор был между 2-мя Open Source-версиями: Gitlab и Gitea, т.к. остальные варианты дороги для self hosting, да и не предоставляют достаточно функций относительно бесплатных вариантов.

Gitlab более развитой, но Gitea его быстро нагоняет и уже очень приятный продукт. При этом Gitlab коммерческая разработка и интересные функции вынесены в платные версии, что не так в случае с Gitea. И тоже важно – Gitea написан на Golang, а Gitlab на Rails. Из-за этого Gitea быстрее и у него меньше системные требования.

CICD движок: Gitea

Варианты: Drone, ArgoCD, Gitea

У Gitea движок аналогичный GitHub/Gitlab, так что смысла искать что-то другое особого нет. Если из-за текущих ограничений не понравится (эта подсистема появилась относительно недавно и активно развивается в Gitea), то можно временно использовать что-то другое (Drone).

Анализатор качества кода: SonarQube, Ktlint

Варианты: detekt, ktlint, sonarqube

sonarqube находит множество проблем качественно. ktlint больше по форматированию, но тоже находит хорошо и используется.

detekt по умолчанию находит как-то слишком много сомнительных вещей. Еще присматриваюсь к нему, пока что нет в шаблоне.

Редактор: Idea Ultimate Edition

Варианты: Idea SE, Idea UE, VS Code

Пока что Idea UE вне конкуренции, но есть надежда на VS Code еще через несколько лет развития.

Основные дополнительные плагины:

  • Kotlin Fill Class
  • SonarLint
  • GraphQL
  • .ignore
  • EnvFile
  • Kubernetes

Технология API: GraphQL

Варианты: plain HTTP, REST, jRPC, GraphQL

GraphQL имеет хорошую типизацию и популярность. Главное отличие от HTTP/REST – это введение нового слоя абстракции – запросы. Он не привносит заметного замедления изначально, а при развитии проекта только проявляет себя еще лучше:

  • можно собрать все запросы, чтобы понять используется что-то или нет
  • клиенты (UI) могут обновлять свои запросы без изменения сервера, что приводит к независимости и меньшему количеству технического долга

Стиль API: CQRS

Варианты: REST, RPC, CRUD, CQRS

GraphQL сам по себе подстегивает стиль CQRS: отдельные запросы и мутации. Опять же с течением времени отдача от такого подхода только увеличивается.

Развертывание приложения: Kubernetes или Podman

Варианты: Kubermetes, Podman, Docker compose, Docker, Systemd service

Если в инфраструктуре уже есть Kubernetes, то очевидный вариант продолжить его использовать. Если нет (например, в домашних условиях), то можно использовать Podman – он так же позволяет создавать поды, аналогичные Кубернетесу, но без всей тяжелой обвязки.

Подробнее в специализированной заметке в блоге “Как запускать серверные приложения в 2023”.

Пакетирование приложения: Docker

Варианты: zip, war, jar, rpm, deb, docker

Т.к. запускаться будет либо через Докер, либо через Кубернетес, то docker очевидный ответ.

Сборка через Jib, т.к. он не требует Docker (удобно для CICD).

Базовые версии

  • JVM: 17 – текущая стабильная
  • Kotlin: 1.8 – текущая версия из генератора приложения
  • Spring Boot 3 (Spring 6) – текущая стабильная
  • Postgres: 15.2 – текущая стабильная

Архитектурный принцип: монолит

Варианты: монолит, микросервис

Начинать нужно всегда с монолита, т.к. так быстрее писать и исправлять. Когда-нибудь потом это можно будет разделить на микросервисы.

Понятно, что если у вас в распоряжении есть S3 API, то не нужно это самостоятельно писать. Можно использовать это как внешний микросервис.

Аналогично, если какой-то код написан на другом языке – не нужно особо мучаться, можно обернуть его в отдельный микросервис.

При этом основной код имеет смысл оставить на первые года развития в монолите. А потом лучше будет понятно нужно ли от этого отходить и, если нужно, то как.

Например, проект Gitea – монолит и вряд ли ожидается, что он станет микросервисным когда-либо.

Модульность: один модуль

Варианты: один модулей, много модулей

Никаких преимуществ от опыта работы с многомодульными проектами не видел. Одни проблемы. Поэтому одного модуля достаточно.

Принцип: структура папок

Основные папки:

  • bin – скрипты, используемые для разработки
  • docs – основная документация (потом можно выносить в вики, если документация будет активно развиваться)
  • deployment – файлы, связанные с деплойментом в Kubernetes или Podman
  • src/main/resources/db/migration – миграции схемы БД Flyway

Принцип: структура пакетов

Варианты: довольно много

Основные пакеты:

  • client – клиенты к внешним системам (обычно http-клиенты, но могут быть и очереди, и что-то другое)
  • config – настройки приложения, Spring и библиотек
  • db – код работы с Postgres
    • dao – jOOQ: ручной код общения с Postgres
    • entity – Spring Data
    • repository – Spring Data
    • sql – jOOQ: сгенерированный код схемы Postgres
  • domain – доменный код
    • “domain name” – в приложении (т.к. это монолит, скорее всего будет несколько доменов)
      • client – адаптер к внешним клиентам в терминах моделей домена
      • model – модели домена
      • service – сервисы домена
  • graphql – код, специфичный для GraphQL
  • service – сервисы уровня приложения
  • utils – классы, которые в идеальном мире должны быть в каких-то библиотеках
  • web – код, специфичный для HTTP

В целом, структура подразумевает четкое разделение кода на уровни – работа с внешней средой, технический код, бизнес-код с учетом обычной специфики приложений.

В отличие от более жестких политик тут предполагается возможность проникновения структур данных между слоями. Это сделано из практических соображений: такого практически не бывает, что клиент полностью поменялся, а структура данных в итоге осталась той же. Даже если такое случиться, то автоматическим рефакторингом это все исправляется. Пуризм же ради пуризма приводит к лишнему коду, что удорожает развитие проекта и поддержку (т.к. это очень важная тема про это есть отдельная заметка в блоге).

Так же можно заметить, что структура пакетов не подразумевает пакетов для интерфейсов. Это из-за того, что интерфейсов практически не ожидается. Интерфейс имеет право на жизнь в данной структуре, если у него уже есть несколько реализаций (тот же принцип отсутствия лишнего кода).

Тестировать структуру пакетов можно при помощи ArchUnit. В данном стартере примера нет, но может быть когда-нибудь появится (я использую на некоторых проектах, но не могу сказать, что год мне нравится, да и ценность автоматической проверки есть только в реально больших коллективах).

Предполагается, что на каждом уровне будет 3-10 файлов. Если больше или меньше, то, в идеале, нужно корректировать структуру пакетов.

Принцип: без пакета с именем приложения

Варианты: с пакетом с именем приложения, без пакета с именем приложения

Например, пусть пакет организации будет name.stepin, а приложение пусть называется kotlin-bootstrap-app. Тогда 2 следующих варианта базовых пакетов для приложения:

  • name.stepin
  • name.stepin.kotlin-bootstrap-app

Второй вариант выглядит менее удачным:

  • это усложняет перемещение классов из приложения в приложение, а класть все исходники в одно дерево файлов мы не планируем
  • путь ограничен 255 и 21 символ мы использовали на пустом месте, в итоге может где-то потом не хватить

Принцип версионирования: семантические версии

Варианты: семантические версии, множество других

Для серверных приложений вполне можно использовать семантические версии, где первая цифра – сломана ли совместимость со старыми клиентами или нет (что соответствует семантическим версиям).

Подробнее в специализированной заметке в блоге “Семантические версии”.

Дополнительные библиотеки: MapStruct? Нет

Варианты: писать самому, MapStruct

Котлин хорошо подходит, чтобы писать это самому.

Подробнее в специализированной заметке в блоге “Как я не сделал KGenMapper”.

Дополнительные библиотеки: hibernate validator

Это библиотека для написания валидаторов. Обычно она нужна и ее хватает. Альтернатив не искал.

Дополнительные библиотеки: mockk

Это хорошая библиотека с полноценной поддержкой Kotlin. Имеется интеграция и с Quarkus. Особых альтернатив нет.

Сборка покрытия тестами: kover

Варианты: jacoco, kover

JaCoCo по сути является стандартом де-факто, но оно не поддерживает некоторые специфичные Kotlin-конструкции, поэтому лучше использовать Kover.

Логирование

Принципы логирования описаны в специализированной заметке в блоге.

Для логирования применяется библиотека log4j-api-kotlin, т.к. она позволяет удобно пользоваться логами:

companion object : Logging

logger.info { "hello $arg" }

Kotlin starter

Для начала Quarkus-варианта шаблона использовался следующий стартер:

  • Kotlin
  • YAML Configuration
  • JDBC Driver - PostgreSQL
  • Agroal - Database connection pool
  • Flyway
  • Quarkus jOOQ
  • RESTEasy Reactive
  • RESTEasy Reactive Jackson
  • REST Client Reactive
  • REST Client Reactive Jackson
  • Hibernate Validator
  • SmallRye Health
  • SmallRye GraphQL
  • SmallRye OpenAPI
  • Micrometer metrics
  • Micrometer Registry Prometheus
  • Jacoco - Code Coverage

Почти все компоненты разобраны выше в архитектурных решениях. Если не разобрано, то мне это показалось слишком скучным/очевидным.

Далее было несколько изменений/дополнений, это видно в истории git.

Так получилось, что сначала делал на Quarkus, решил ту версию так же сохранить, там весьма много сделано.

Код

Репозитарий с кодом. Вариации:

В коде несколько больше, чем описано решений здесь – и как с зависимостями работать (через конструктор), и как тесты писать.

Видео

На видео более низкоуровневый (пофайловый) разбор шаблона: