← Назад к вопросам
Какой используешь подход к разделению монолитных модулей?
2.7 Senior🔥 111 комментариев
#REST API и микросервисы#SOLID и паттерны проектирования
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Подходы к разделению монолитных модулей
Переход от монолита к микросервисам или модульной архитектуре — это один из самых важных архитектурных рефакторингов. Я расскажу о практических подходах, которые использовал.
Основные стратегии разделения
1. Strangler Fig Pattern (наиболее безопасный подход)
Постепенно заменяем части монолита новыми микросервисами, оборачивая старый код.
// Старый монолит
public class LegacyMonolith {
public OrderResponse processOrder(OrderRequest request) {
// Все бизнес-логика в одном месте
validateOrder(request);
checkInventory(request);
processPayment(request);
sendNotification(request);
return new OrderResponse();
}
}
// Шаг 1: Создаем новый микросервис для платежей
@RestController
@RequestMapping("/api/payments")
public class PaymentService {
@PostMapping("/process")
public PaymentResponse processPayment(@RequestBody PaymentRequest request) {
// Новая реализация
return paymentUseCase.execute(request);
}
}
// Шаг 2: Фасад перенаправляет запросы
@RestController
public class OrderFacade {
private final PaymentService paymentService;
private final LegacyMonolith legacyMonolith;
@PostMapping("/orders")
public OrderResponse createOrder(@RequestBody OrderRequest request) {
// Валидация и инвентарь из монолита
legacyMonolith.validateOrder(request);
legacyMonolith.checkInventory(request);
// Платежи в новый микросервис
PaymentResponse payment = paymentService.processPayment(
new PaymentRequest(request.getAmount())
);
// Уведомления из монолита
legacyMonolith.sendNotification(request);
return new OrderResponse();
}
}
// Шаг 3: Постепенно выделяем остальное
// Инвентарь -> отдельный микросервис
// Уведомления -> отдельный микросервис
// Валидация -> отдельный микросервис
Преимущества:
- Низкий риск: старая система продолжает работать
- Поэтапность: разделение по частям
- Откат: всегда можно вернуться к монолиту
- Кэширование: фасад может кэшировать результаты
2. Domain-Driven Design (выделение по доменам бизнеса)
Это мой предпочитаемый подход, основанный на Bounded Contexts.
// СТРУКТУРА МОНОЛИТА ДО
// monolith/
// ├── OrderService
// ├── PaymentService
// ├── InventoryService
// ├── NotificationService
// ├── UserService
// └── ReportService
// Все смешано в одной кодовой базе!
// СТРУКТУРА ПОСЛЕ (микросервисы по доменам)
// order-service/
// ├── domain/
// │ ├── Order
// │ ├── OrderStatus
// │ └── OrderRepository (interface)
// ├── application/
// │ └── CreateOrderUseCase
// ├── infrastructure/
// │ ├── OrderRepositoryImpl
// │ └── OrderController
// └── tests/
// payment-service/
// ├── domain/
// │ ├── Payment
// │ └── PaymentGateway (interface)
// ├── application/
// │ └── ProcessPaymentUseCase
// └── infrastructure/
// └── PaymentController
// inventory-service/
// ├── domain/
// │ ├── Stock
// │ └── StockRepository (interface)
// └── ...
Практическая реализация:
// СТАРЫЙ МОНОЛИТ (AntiPattern)
public class OrderService {
@Autowired
private OrderRepository orderRepository; // DB
@Autowired
private PaymentGateway paymentGateway; // External API
@Autowired
private InventoryService inventoryService; // Internal service
@Autowired
private NotificationSender notificationSender; // External API
@Autowired
private ReportGenerator reportGenerator; // Another service
// Все зависимости смешаны!
public Order createOrder(OrderDTO dto) {
// 100 строк смешанной логики
}
}
// НОВЫЙ ПОРЯДОК SERVICE (DDD)
// Order Bounded Context
@Service
public class CreateOrderUseCase {
private final OrderRepository orderRepository; // Domain repository
private final PaymentServiceClient paymentClient; // External (interface)
private final InventoryServiceClient inventoryClient; // External (interface)
public OrderResponse execute(CreateOrderCommand command) {
// Логика создания заказа
Order order = new Order(command.getItems());
order.calculateTotal();
order.validate();
orderRepository.save(order);
return new OrderResponse(order);
}
}
// В отдельном микросервисе payment-service/
@Service
public class ProcessPaymentUseCase {
private final PaymentGateway paymentGateway;
private final PaymentRepository paymentRepository;
public PaymentResponse execute(ProcessPaymentCommand command) {
Payment payment = new Payment(command.getOrderId(), command.getAmount());
try {
paymentGateway.process(payment); // External payment provider
payment.markAsSuccessful();
} catch (PaymentException e) {
payment.markAsFailed();
// Publish event: PaymentFailed
publishEvent(new PaymentFailedEvent(payment));
}
paymentRepository.save(payment);
return new PaymentResponse(payment);
}
}
Взаимодействие между сервисами через события:
// Order Service публикует события
@Service
public class OrderCreatedEventPublisher {
private final EventBus eventBus;
public void publishOrderCreated(Order order) {
eventBus.publish(new OrderCreatedEvent(
order.getId(),
order.getCustomerId(),
order.getTotal()
));
}
}
// Payment Service подписывается на события
@Service
public class OrderCreatedEventHandler {
private final ProcessPaymentUseCase processPaymentUseCase;
@EventListener
public void handle(OrderCreatedEvent event) {
// Автоматически обрабатываем платеж
processPaymentUseCase.execute(
new ProcessPaymentCommand(event.getOrderId(), event.getTotal())
);
}
}
3. Трехслойное разделение (Feature-based)
Разделяем по функциональности, каждый микросервис полностью отвечает за свою feature.
// СТРУКТУРА
// payment-service/
// ├── rest/
// │ └── PaymentController
// ├── service/
// │ ├── PaymentProcessingService
// │ └── PaymentValidationService
// ├── repository/
// │ ├── PaymentRepository
// │ └── PaymentJpaRepository
// ├── entity/
// │ └── PaymentEntity
// ├── dto/
// │ ├── PaymentRequest
// │ └── PaymentResponse
// └── exception/
// ├── PaymentException
// └── PaymentNotValidException
// МИНУСЫ такого подхода:
// - Слишком focused на технологии
// - Может привести к inconsistency
// - Сложнее масштабировать
4. Модульный монолит (перед разделением на микросервисы)
Переход за промежуточный этап перед полным разделением.
// СТРУКТУРА МОДУЛЬНОГО МОНОЛИТА
// src/main/java/com/company/
// ├── core/ // Общая инфраструктура
// │ ├── config/
// │ ├── exception/
// │ └── util/
// ├── order/ // Модуль 1: Order BC
// │ ├── domain/
// │ ├── application/
// │ ├── infrastructure/
// │ ├── presentation/
// │ └── OrderModule.java
// ├── payment/ # Модуль 2: Payment BC
// │ ├── domain/
// │ ├── application/
// │ ├── infrastructure/
// │ └── presentation/
// └── shared/ # Shared kernel
// ├── event/
// └── domain/
public interface OrderModule {}
@Configuration
public class OrderModuleConfig implements OrderModule {
@Bean
public CreateOrderUseCase createOrderUseCase(OrderRepository repo) {
return new CreateOrderUseCase(repo);
}
}
@SpringBootApplication(scanBasePackageClasses = {
OrderModule.class,
PaymentModule.class,
InventoryModule.class
})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Практический процесс разделения
Фаза 1: Анализ (2-4 недели)
// Шаг 1: Нарисовать текущую архитектуру
// Какие классы тесно связаны?
// Какие могут работать независимо?
// Шаг 2: Определить Bounded Contexts
// Order Context
// Payment Context
// Inventory Context
// Notification Context
Фаза 2: Подготовка монолита (2-3 недели)
// Рефакторим монолит в модули
// 1. Вводим интерфейсы (Dependency Inversion)
// 2. Уменьшаем циклические зависимости
// 3. Вводим Event Bus для async коммуникации
// Было
public class PaymentService {
public void processPayment(Order order) {
// Прямой вызов
inventoryService.decreaseStock(order);
notificationService.sendEmail(order);
}
}
// Стало
public class PaymentService {
private final EventBus eventBus;
public void processPayment(Order order) {
// Событие вместо прямого вызова
eventBus.publish(new PaymentProcessedEvent(order));
}
}
Фаза 3: Выделение первого микросервиса (1-2 недели)
// Выбираем самый независимый модуль
// Обычно это Payment или Notification
// 1. Создаем новый Spring Boot проект
// 2. Копируем domain logic
// 3. Добавляем REST контроллер
// 4. Заменяем в монолите на REST клиент
// 5. Развертываем
// 6. Мониторим
public class PaymentServiceClient {
private final RestTemplate restTemplate;
public PaymentResponse processPayment(Order order) {
return restTemplate.postForObject(
"http://payment-service:8080/api/payments/process",
new PaymentRequest(order),
PaymentResponse.class
);
}
}
Фаза 4: Итеративное выделение остального
// Процесс повторяется для каждого модуля
// Обычно выделяем 1-2 модуля в спринт
// Контролируем quality с автотестами
// Чек-лист для каждого выделения:
// ☐ Unit тесты покрывают бизнес логику (90%+)
// ☐ Integration тесты с БД
// ☐ E2E тесты через REST API
// ☐ Граммотная обработка ошибок
// ☐ Логирование и мониторинг
// ☐ Документация API (Swagger/OpenAPI)
// ☐ Backward compatibility для других сервисов
Когда разделять, а когда нет
✅ Разделяй, если:
- Разные команды разрабатывают модули
- Разные сроки развертывания
- Разная нагрузка на модули (масштабирование)
- Разные технологические стеки
- Разные требования к SLA/latency
❌ Не разделяй, если:
- Модули очень тесно связаны
- Очень частые cross-module запросы
- Нет опыта с микросервисами
- Маленькая команда (< 2 разработчиков на сервис)
Мой практический подход
// Начинаем с модульного монолита
// Core: database, config, logging, security
// Features: order, payment, inventory (отдельные папки)
// Через 6-12 месяцев выделяем первый микросервис
// Обычно это то, что часто падает или требует отдельного масштабирования
// Используем async Events для слабой связанности
// Используем API Gateway для routing
// Используем Circuit Breaker для resilience
public class OrderService {
private final PaymentServiceClient paymentClient;
private final CircuitBreaker circuitBreaker;
public Order createOrder(OrderRequest request) {
Order order = new Order(request);
orderRepository.save(order);
// С resilience
try {
circuitBreaker.executeSupplier(() ->
paymentClient.processPayment(order)
);
} catch (Exception e) {
// Payment service down, но заказ создан
// Retry позже через event
eventBus.publish(new PaymentPendingEvent(order));
}
return order;
}
}
Итоговые рекомендации
- Начни с DDD: правильно определи Bounded Contexts
- Используй Strangler Fig: постепенный переход, не вся за раз
- Вводи события: слабо связанная архитектура
- Тестируй всё: unit, integration, E2E на каждом этапе
- Мониторь: distributed tracing, логи, metrics
- Dokumentiruj: API, архитектурные решения
- Будь готов к откату: быстро вернуться, если что-то пошло не так