Новый взгляд на монолит: современные паттерны и примеры кода от экспертов

Статья о современных подходах к монолитной архитектуре (Modulith, DDD), их преимуществах перед преждевременным переходом на микросервисы, с практическими примерами кода, иллюстрирующими модульность и слабую связанность внутри единой кодовой базы.
В эпоху тотального увлечения микросервисами слово "монолит" стало почти ругательным. Однако практика последних лет показала, что слепое следование тренду приводит к сложным распределенным системам, накладным расходам на сеть, проблемам с согласованностью данных и адским сложностям в отладке. Это вызвало ренессанс монолитной архитектуры, но не той, что была в 2000-х, а современной, модульной и хорошо структурированной. Эксперты все чаще говорят о "монолите будущего" — это высокоорганизованная система, сочетающая простоту развертывания единой кодовой базы с четкими внутренними границами, готовыми к возможному разделению в будущем, если это будет оправдано.

Ключевой современный паттерн — это Modulith (модульный монолит). Приложение строится как единое целое, но его кодовая база жестко разделена на модули (features, domains), каждый из которых инкапсулирует свою бизнес-логику, данные и API. Модули взаимодействуют через четко определенные интерфейсы, а не через прямые вызовы внутренних классов. Это позволяет добиться высокой связности внутри модуля и низкой связанности между модулями. Для Java-экосистемы эта концепция популяризирована Spring Modulith, для .NET — аналогичные подходы с использованием вертикальных срезов (Vertical Slices).

Рассмотрим пример на псевдокоде, вдохновленном Spring Boot и Modulith. У нас есть монолит интернет-магазина с модулями `catalog`, `order` и `inventory`.

```
// Модуль catalog (package: com.shop.catalog)
package com.shop.catalog;

@ModulithicModule // Условная аннотация, обозначающая границу модуля
public interface CatalogService {
 Product getProductById(Long id);
}

@Service
public class CatalogServiceImpl implements CatalogService {
 @Autowired
 private ProductRepository repository;

 @Override
 public Product getProductById(Long id) {
 return repository.findById(id).orElseThrow();
 }
}

// Модуль order (package: com.shop.order)
package com.shop.order;

@Service
public class OrderService {
 // Внедрение зависимости через интерфейс из другого модуля
 private final CatalogService catalogService;
 private final InventoryClient inventoryClient; // Еще один интерфейс

 public OrderService(CatalogService catalogService, InventoryClient client) {
 this.catalogService = catalogService;
 this.inventoryClient = client;
 }

 public Order createOrder(Long productId, int quantity) {
 // Явный вызов через публичный API модуля catalog
 Product product = catalogService.getProductById(productId);

 // Асинхронная проверка через "внутренний" клин (может быть как in-memory, так и HTTP в будущем)
 boolean inStock = inventoryClient.checkAvailability(productId, quantity);
 if (!inStock) {
 throw new InsufficientStockException();
 }

 // Создание заказа...
 Order newOrder = new Order(product, quantity);
 // Сохранение в своей БД (возможно, отдельной схеме)
 return orderRepository.save(newOrder);
 }
}
```

Обратите внимание: `OrderService` не знает о реализации `CatalogServiceImpl` или о том, как устроено хранилище товаров. Он использует только публичный контракт. Это позволяет в будущем вынести модуль `catalog` в отдельный микросервис, заменив внедрение бина на вызов REST-клиента или сообщение в брокере, с минимальными изменениями в коде `OrderService`.

Еще один мощный паттерн — это Domain-Driven Design (DDD) внутри монолита. Четкое разделение на ограниченные контексты (Bounded Context), агрегаты (Aggregates) и доменные события (Domain Events) создает идеальную основу для модульности. События, публикуемые одним модулем и потребляемые другим через in-memory шину событий (например, Spring ApplicationEvent), позволяют организовать слабосвязанное взаимодействие. В будущем эту шину можно заменить на Kafka или RabbitMQ, превратив монолит в распределенную систему событий.

Пример доменного события в том же магазине:

```
// В модуле order, после создания заказа
package com.shop.order;

@Service
public class OrderService {
 @Autowired
 private ApplicationEventPublisher eventPublisher;

 public Order createOrder(...) {
 // ... логика создания
 Order order = orderRepository.save(newOrder);
 // Публикация события внутри того же процесса
 eventPublisher.publishEvent(new OrderCreatedEvent(order.getId(), order.getProductId(), order.getQuantity()));
 return order;
 }
}

// Слушатель в модуле inventory
package com.shop.inventory;

@Component
public class InventoryEventHandler {
 @EventListener
 public void handleOrderCreated(OrderCreatedEvent event) {
 // Обновление уровня запасов в своей БД
 inventoryRepository.reserveStock(event.getProductId(), event.getQuantity());
 }
}
```

Такой подход обеспечивает согласованность данных в рамках транзакции базы данных монолита, но при этом логически разделяет ответственность.

Для управления данными в модульном монолите эксперты рекомендуют подход "одна база данных, но отдельные схемы/таблицы на модуль" (Database per Service inside a single DB instance). Каждый модуль работает только со своими таблицами, доступ к которым осуществляется через его приватный репозиторий. Общие справочники могут быть выделены в отдельный модуль-библиотеку. Это предотвращает создание спагети-кода с JOIN по двадцати таблицам из разных доменов.

Преимущества такого подхода очевидны: простота разработки (один проект), отладки (сквозной трассировкой в одном процессе), развертывания (один артефакт) и обеспечения транзакционной согласованности (ACID). При этом система сохраняет гибкость. Когда нагрузка на конкретный модуль (например, "каталог" в период распродаж) резко возрастает, его можно относительно безболезненно выделить в отдельный сервис, так как границы уже прочерчены, а зависимости — инвертированы через интерфейсы.

Вывод экспертов однозначен: начинайте с модульного монолита. Это архитектура по умолчанию для большинства новых проектов. Она позволяет быстро выйти на рынок, не увязая в сложностях распределенных систем. Микросервисы — это дорогое архитектурное решение, которое должно быть принято осознанно, только когда монолит действительно перестает справляться с конкретными, измеримыми нагрузками или требованиями к независимому масштабированию и развертыванию компонентов. Современный монолит — это не шаг назад, а зрелый, прагматичный выбор.
495 4

Комментарии (8)

avatar
17g9ezjz 01.04.2026
Отличная статья! Жду продолжения с конкретными паттернами и примерами разбиения на модули.
avatar
dderrc 02.04.2026
Интересно, а как быть с разным масштабированием компонентов? В монолите ведь всё масштабируется целиком.
avatar
33mwn3yjt5d 02.04.2026
Наконец-то здравый взгляд на архитектуру! Микросервисы — не панацея, а хорошо структурированный монолит часто выигрывает в скорости разработки.
avatar
iyn0kvbc 02.04.2026
Ключевая мысль — не «или-или», а правильный выбор для конкретной задачи. Спасибо за взвешенный подход!
avatar
em7okkvo 02.04.2026
Согласен. Главное — модульность и чистая архитектура внутри монолита. Тогда и масштабироваться, и разделять потом проще.
avatar
clj1jjng 02.04.2026
Всё верно. Мы перегрели с микросервисами, получили distributed monolith. Возврат к модульному монолиту — логичный шаг.
avatar
h5xp0n330 03.04.2026
А есть ли примеры на Go или Rust? Статья в основном про Java-экосистему, хотелось бы шире.
avatar
xiswrb5g 03.04.2026
Сомневаюсь. Для больших команд и независимых релизов микросервисы всё равно лучше. Монолит — шаг назад в управлении.
Вы просмотрели все комментарии