The post has been translated automatically. Original language: Russian
Why not two separate repositories?
When a project has two "faces" — an administrative panel and a user interface — the question arises: how to separate them? Two separate repositories is the most obvious solution, but it creates problems.:
- Code duplication. Entities, converters, common utilities — all this is needed both there and there.
- Synchronization of changes. If you change the `Course` model, update both repositories.
- Dependency management. We need an internal Maven/Gradle repository for shared code.
Our monorepo structure
lms/ ← Gradle root project
├── api/ ← common API contracts, security models
├── common/ ← Common business components
├── back-office/ ← Spring Boot application for managers
├── front-office/ ← Spring Boot application for users
└── build.gradle, general dependencies, Flyway configThe `api` module contains:
- Path constants (`PathConstants.java `)
- Security models (`Permission.java`, `Role.java`)
- Common exceptions and their handlers
The `common` module contains:
- Entities that are used in both applications (`Course`, `User`, `Enrollment`)
- Repositories and services for these entities
- Base classes (`BaseSpecification<T>`, `BaseConverter<E, R>`)
'back-office` and `front-office' are full—fledged Spring Boot applications, each with its own `application.yml', its own controllers and its own business logic. They depend on `common` and `api`, but are independent of each other.
Separation of business logic
One of the key issues: what goes into `common` and what remains in `back-office'/`front-office'?
Rule: in the `common` there are entities and operations that are needed by both applications. In separate modules, something that is specific to the context.
Flyway and migrations
Both services use the same database. We keep migrations in the `common` module — they are applied at the start of any of the applications. To avoid version conflicts during parallel development, we added a custom version generator to the root build.gradle`:
// Generates a version like V{timestamp}__ when creating a new migration
task prefixNewMigrations {
// ...
}This allows two developers to create migrations in parallel without conflicting version numbers.
Two applications — two ports, one database
During local development:
- 'back-office` starts on port `8080`
- `front-office` — on `8081'
- Both connect to one PostgreSQL and one keycloak. CI/CD are two separate Docker images that are deployed independently. If you need to update only the front office, we will deploy only it.
Results
We didn't reinvent the wheel — everything described in the article is based on proven patterns. But putting them together into a working architecture that scales and doesn't turn into chaos is the main task.
The three things that saved us the most time:
1. Feature-based package structure — when adding a new feature, it is immediately clear where to put what. When reviewing, you can immediately see what is affected.
2. API Interface + Controller — Swagger and access rights are separate from controller logic. Changing the documentation does not mean touching the business code.
3. Mono-repo with a clear separation of common/back-office/front—office - no duplication, no problems with synchronization, while applications are deployed independently.
If you are building something similar - a corporate portal, a platform with different user roles, or a system with external content — we hope this experience will be useful.
Stack: Java 21 · Spring Boot 3.5 · PostgreSQL · Minio · Keycloak · Gradle
Почему не два отдельных репозитория
Когда у проекта есть два «лица» — административная панель и пользовательский интерфейс — возникает вопрос: как их разделить? Два отдельных репозитория — самое очевидное решение, но оно создаёт проблемы:
- Дублирование кода. Сущности, конвертеры, общие утилиты — всё это нужно и там, и там
- Синхронизация изменений. Меняешь модель `Course` — обновляй оба репозитория
- Управление зависимостями. Нужен внутренний Maven/Gradle репозиторий для общего кода
Наша структура монорепо
lms/ ← корневой Gradle проект
├── api/ ← общие API-контракты, модели безопасности
├── common/ ← общие бизнес-компоненты
├── back-office/ ← Spring Boot приложение для менеджеров
├── front-office/ ← Spring Boot приложение для пользователей
└── build.gradle ← общие зависимости, Flyway конфиг`api` модуль содержит:
- Константы путей (`PathConstants.java`)
- Модели безопасности (`Permission.java`, `Role.java`)
- Общие исключения и их обработчики
`common` модуль содержит:
- Сущности, которые используются в обоих приложениях (`Course`, `User`, `Enrollment`)
- Репозитории и сервисы для этих сущностей
- Базовые классы (`BaseSpecification<T>`, `BaseConverter<E, R>`)
`back-office` и `front-office` — это полноценные Spring Boot приложения, каждое со своим `application.yml`, своими контроллерами и своей бизнес-логикой. Они зависят от `common` и `api`, но не зависят друг от друга.
Разделение бизнес-логики
Один из ключевых вопросов: что идёт в `common`, а что остаётся в `back-office`/`front-office`?
Правило: в `common` идут сущности и операции, которые нужны обоим приложениям. В отдельные модули — то, что специфично для контекста.
Flyway и миграции
Обе службы используют одну базу данных. Миграции мы держим в `common` модуле — они применяются при старте любого из приложений. Чтобы не было конфликтов версий при параллельной разработке, мы добавили кастомный генератор версий в корневой `build.gradle`:
// Генерирует версию вида V{timestamp}__ при создании новой миграции
task prefixNewMigrations {
// ...
}Это позволяет двум разработчикам создавать миграции параллельно без конфликтов в номерах версий.
Два приложения — два порта, одна БД
При локальной разработке:
- `back-office` запускается на порту `8080`
- `front-office` — на `8081`
- Оба подключаются к одному PostgreSQL и одному KeycloakВ CI/CD это два отдельных Docker-образа, которые деплоятся независимо. Если нужно обновить только фронт-офис — деплоим только его.
Итоги
Мы не изобретали велосипед — всё описанное в статье основано на проверенных паттернах. Но собрать их вместе в работающую архитектуру, которая масштабируется и не превращается в хаос — это и есть основная задача.
Три вещи, которые сэкономили нам больше всего времени:
1. Feature-based структура пакетов — при добавлении новой фичи сразу понятно, куда что класть. При ревью сразу видно, что затронуто.
2. API Interface + Controller — Swagger и права доступа отделены от логики контроллера. Изменить документацию не значит трогать бизнес-код.
3. Монорепо с чётким разделением common/back-office/front-office — нет дублирования, нет проблем с синхронизацией, при этом приложения деплоятся независимо.
Если вы строите что-то похожее — корпоративный портал, платформу с разными ролями пользователей, систему с внешним контентом — надеемся, этот опыт будет полезен.
Стек: Java 21 · Spring Boot 3.5 · PostgreSQL · Minio · Keycloak · Gradle