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

Как общаются между собой сервисы в монолите

2.0 Middle🔥 111 комментариев
#SOLID и паттерны проектирования

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

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Межсервисное взаимодействие в монолите

В монолитном приложении разные компоненты/модули живут в одном процессе и одной JVM. Это отличается от микросервисной архитектуры, где сервисы общаются по сети. Рассмотрю основные подходы к взаимодействию компонентов в монолите.

1. Синхронное взаимодействие через интерфейсы (САМОЕ ПРОСТОЕ)

Прямой вызов методов через зависимости (Dependency Injection).

// Интерфейс контракта
public interface UserService {
    User getUserById(Long id);
    void updateUser(User user);
}

// Реализация
@Service
public class UserServiceImpl implements UserService {
    @Override
    public User getUserById(Long id) {
        // Бизнес-логика
        return new User(id, "John");
    }
    
    @Override
    public void updateUser(User user) {
        // Сохранение
    }
}

// Потребитель сервиса (инъекция зависимости)
@Service
public class OrderService {
    private final UserService userService;
    
    public OrderService(UserService userService) {
        this.userService = userService;
    }
    
    public void createOrder(Long userId, Order order) {
        User user = userService.getUserById(userId);  // Прямой вызов
        // Создание заказа для пользователя
    }
}

Преимущества:

  • Простота и прямолинейность
  • Отсутствие сетевых задержек
  • Легкое тестирование (mock интерфейса)

Недостатки:

  • Плотная связанность
  • Сложно развести на микросервисы позже
  • Все сервисы падают вместе

2. Событийно-ориентированное взаимодействие (РЕКОМЕНДУЕТСЯ)

Сервисы публикуют события, на которые подписываются другие. Позволяет ослабить связанность.

Пример 1: Синхронные события

// Событие
public class OrderCreatedEvent {
    private Long orderId;
    private Long userId;
    private BigDecimal amount;
    
    public OrderCreatedEvent(Long orderId, Long userId, BigDecimal amount) {
        this.orderId = orderId;
        this.userId = userId;
        this.amount = amount;
    }
    
    // Getters
    public Long getOrderId() { return orderId; }
    public Long getUserId() { return userId; }
    public BigDecimal getAmount() { return amount; }
}

// Издатель события
@Service
public class OrderService {
    private final ApplicationEventPublisher eventPublisher;
    
    public OrderService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }
    
    public void createOrder(Long userId, Order order) {
        // Создание заказа
        Order savedOrder = saveOrder(order);
        
        // Публикуем событие
        OrderCreatedEvent event = new OrderCreatedEvent(
            savedOrder.getId(),
            userId,
            order.getAmount()
        );
        eventPublisher.publishEvent(event);
    }
    
    private Order saveOrder(Order order) {
        // Сохранение в БД
        return order;
    }
}

// Слушатель события
@Component
public class OrderEventListener {
    private final NotificationService notificationService;
    private final ReportService reportService;
    
    public OrderEventListener(NotificationService notificationService,
                             ReportService reportService) {
        this.notificationService = notificationService;
        this.reportService = reportService;
    }
    
    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        // Отправляем уведомление
        notificationService.sendOrderConfirmation(event.getUserId(), event.getOrderId());
        
        // Обновляем отчёты
        reportService.recordSale(event.getAmount());
    }
}

Пример 2: Асинхронные события

@Component
public class OrderEventListener {
    private final NotificationService notificationService;
    
    @EventListener
    @Async  // Выполняется в отдельном потоке
    public void onOrderCreated(OrderCreatedEvent event) {
        try {
            // Тяжёлая операция в отдельном потоке
            notificationService.sendEmailNotification(event.getUserId());
        } catch (Exception e) {
            // Обработка ошибок
            log.error("Failed to send notification", e);
        }
    }
}

// Включаем асинхронные события в конфигурации
@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor taskExecutor() {
        return new ThreadPoolTaskExecutor();
    }
}

3. Событийный шаг со стабильностью (Outbox Pattern)

Для гарантии доставки событий используется Outbox Pattern.

// Сущность заказа
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue
    private Long id;
    private Long userId;
    private BigDecimal amount;
    // ...
}

// Таблица outbox для событий
@Entity
@Table(name = "outbox_events")
public class OutboxEvent {
    @Id
    @GeneratedValue
    private Long id;
    private String eventType;
    private String payload;  // JSON
    private LocalDateTime createdAt;
    private Boolean published = false;
    
    public OutboxEvent(String eventType, String payload) {
        this.eventType = eventType;
        this.payload = payload;
        this.createdAt = LocalDateTime.now();
    }
}

// Сервис с транзакцией
@Service
@Transactional
public class OrderService {
    private final OrderRepository orderRepo;
    private final OutboxEventRepository outboxRepo;
    private final ObjectMapper mapper;
    
    public void createOrder(Long userId, Order order) {
        // 1. Сохраняем заказ
        Order savedOrder = orderRepo.save(order);
        
        // 2. В одной транзакции сохраняем событие в outbox
        OrderCreatedEvent event = new OrderCreatedEvent(
            savedOrder.getId(), userId, order.getAmount()
        );
        OutboxEvent outboxEvent = new OutboxEvent(
            "OrderCreatedEvent",
            mapper.writeValueAsString(event)
        );
        outboxRepo.save(outboxEvent);
    }
}

// Отдельный поток читает outbox и публикует события
@Service
public class OutboxPoller {
    private final OutboxEventRepository outboxRepo;
    private final ApplicationEventPublisher eventPublisher;
    private final ObjectMapper mapper;
    
    @Scheduled(fixedDelay = 5000)  // Каждые 5 секунд
    public void pollOutbox() {
        List<OutboxEvent> unpublished = outboxRepo.findByPublishedFalse();
        
        for (OutboxEvent event : unpublished) {
            try {
                // Десериализуем и публикуем
                Object payload = mapper.readValue(event.getPayload(), Object.class);
                eventPublisher.publishEvent(payload);
                
                event.setPublished(true);
                outboxRepo.save(event);
            } catch (Exception e) {
                log.error("Failed to publish event", e);
            }
        }
    }
}

4. Сообщения через Message Broker (для будущих микросервисов)

Дажев монолите можно использовать RabbitMQ или Kafka, чтобы потом было легче перейти на микросервисы.

@Configuration
public class RabbitConfig {
    public static final String ORDER_QUEUE = "order-queue";
    public static final String ORDER_EXCHANGE = "order-exchange";
    
    @Bean
    public Queue orderQueue() {
        return new Queue(ORDER_QUEUE);
    }
    
    @Bean
    public TopicExchange orderExchange() {
        return new TopicExchange(ORDER_EXCHANGE);
    }
    
    @Bean
    public Binding binding() {
        return BindingBuilder.bind(orderQueue())
            .to(orderExchange())
            .with("order.*");
    }
}

// Издатель
@Service
public class OrderService {
    private final RabbitTemplate rabbitTemplate;
    
    public void createOrder(Long userId, Order order) {
        Order saved = saveOrder(order);
        
        // Отправляем в очередь
        rabbitTemplate.convertAndSend(RabbitConfig.ORDER_EXCHANGE, "order.created",
            new OrderCreatedEvent(saved.getId(), userId, saved.getAmount())
        );
    }
}

// Слушатель
@Component
public class OrderEventListener {
    @RabbitListener(queues = RabbitConfig.ORDER_QUEUE)
    public void handleOrderCreated(OrderCreatedEvent event) {
        // Обработка события
        System.out.println("Order created: " + event.getOrderId());
    }
}

5. Façade/Orchestrator Pattern

Если нужна сложная многошаговая координация.

@Service
public class CheckoutOrchestrator {
    private final OrderService orderService;
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    private final ShippingService shippingService;
    
    public CheckoutOrchestrator(
            OrderService orderService,
            PaymentService paymentService,
            InventoryService inventoryService,
            ShippingService shippingService) {
        this.orderService = orderService;
        this.paymentService = paymentService;
        this.inventoryService = inventoryService;
        this.shippingService = shippingService;
    }
    
    @Transactional
    public CheckoutResult checkout(Long userId, Cart cart) {
        try {
            // 1. Создаём заказ
            Order order = orderService.createOrder(userId, cart.getItems());
            
            // 2. Проверяем инвентарь
            if (!inventoryService.reserve(cart.getItems())) {
                throw new InsufficientStockException();
            }
            
            // 3. Обрабатываем платёж
            PaymentResult payment = paymentService.charge(
                userId,
                cart.getTotalAmount()
            );
            
            if (!payment.isSuccessful()) {
                inventoryService.releaseReservation(order.getId());
                throw new PaymentFailedException();
            }
            
            // 4. Создаём доставку
            Shipment shipment = shippingService.createShipment(order);
            
            return new CheckoutResult(order, payment, shipment);
        } catch (Exception e) {
            // Откатываем на ошибке
            log.error("Checkout failed", e);
            throw e;
        }
    }
}

Сравнение подходов

ПодходСинхронностьСвязанностьТестируемостьМасштабируемостьПростота
Прямой вызовСинхронныйВысокаяХорошаяСлабаяВысокая
Spring EventsСинхронныйНизкаяОтличнаяХорошаяХорошая
@Async EventsАсинхронныйНизкаяОтличнаяХорошаяХорошая
Outbox PatternАсинхронныйНизкаяХорошаяХорошаяСредняя
Message BrokerАсинхронныйНизкаяОтличнаяОтличнаяСредняя
OrchestratorСинхронныйСредняяХорошаяСредняяСредняя

Best Practices

// ✅ Используй интерфейсы для слабой связанности
public interface UserService {
    User getUser(Long id);
}

// ✅ Публикуй события для асинхронных операций
eventPublisher.publishEvent(new UserRegisteredEvent(user));

// ✅ Обрабатывай ошибки в слушателях
@EventListener
@Async
public void onEvent(MyEvent event) {
    try {
        // логика
    } catch (Exception e) {
        log.error("Failed to handle event", e);
    }
}

// ❌ Не создавай циклические зависимости
// ServiceA → ServiceB → ServiceA

// ❌ Не игнорируй ошибки
// try { /* что-то */ } catch (Exception e) { }

Выводы

  1. Для простых случаев — используй синхронный вызов через интерфейсы
  2. Для ослабления связанности — используй Spring Events
  3. Для асинхронности — добавь @Async или Outbox Pattern
  4. Для будущих микросервисов — используй Message Broker (RabbitMQ/Kafka)
  5. Для сложной координации — применяй Orchestrator Pattern
  6. Всегда обрабатывай ошибки в асинхронных операциях
  7. Тестируй каждый способ взаимодействия через unit и integration тесты