← Назад к вопросам

Какой используешь подход к разделению монолитных модулей?

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;
    }
}

Итоговые рекомендации

  1. Начни с DDD: правильно определи Bounded Contexts
  2. Используй Strangler Fig: постепенный переход, не вся за раз
  3. Вводи события: слабо связанная архитектура
  4. Тестируй всё: unit, integration, E2E на каждом этапе
  5. Мониторь: distributed tracing, логи, metrics
  6. Dokumentiruj: API, архитектурные решения
  7. Будь готов к откату: быстро вернуться, если что-то пошло не так
Какой используешь подход к разделению монолитных модулей? | PrepBro