← Назад к вопросам
Как общаются между собой сервисы в монолите
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) { }
Выводы
- Для простых случаев — используй синхронный вызов через интерфейсы
- Для ослабления связанности — используй Spring Events
- Для асинхронности — добавь @Async или Outbox Pattern
- Для будущих микросервисов — используй Message Broker (RabbitMQ/Kafka)
- Для сложной координации — применяй Orchestrator Pattern
- Всегда обрабатывай ошибки в асинхронных операциях
- Тестируй каждый способ взаимодействия через unit и integration тесты