← Назад к вопросам
На что обращаешь внимание при разделении монолита на микросервисы?
2.7 Senior🔥 141 комментариев
#REST API и микросервисы#SOLID и паттерны проектирования
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
На что обращаешь внимание при разделении монолита на микросервисы?
Краткий ответ
Разделение монолита на микросервисы требует системного анализа. Главное — это правильно определить boundary каждого сервиса, обеспечить слабую связность, управлять распределённым состоянием и спланировать развёртывание и мониторинг.
1. Анализ границ и контекстов (Domain-Driven Design)
Domain-Driven Design (DDD)
Примени концепцию Bounded Context из DDD:
Пример: E-commerce система
┌────────────────────────────────────────────────┐
│ Monolith (текущее состояние) │
├────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Users & Authentication │ │
│ │ - createUser() │ │
│ │ - login() │ │
│ │ - updateProfile() │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Products & Catalog │ │
│ │ - getProducts() │ │
│ │ - updateInventory() │ │
│ │ - searchProducts() │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Orders & Payments │ │
│ │ - createOrder() │ │
│ │ - processPayment() │ │
│ │ - trackOrder() │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Notifications │ │
│ │ - sendEmail() │ │
│ │ - sendSMS() │ │
│ └─────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────┘
После разделения на микросервисы:
┌─────────────────┬────────────────┬──────────────┬─────────────────┐
│ │ │ │ │
│ User Service │ Product Service│ Order Service│Notification Srv │
│ :8001 │ :8002 │ :8003 │ :8004 │
│ │ │ │ │
│ ┌─────────────┐ │ ┌────────────┐ │ ┌──────────┐ │ ┌─────────────┐ │
│ │ Users DB │ │ │ Products DB│ │ │ Orders DB│ │ │ Events Queue│ │
│ └─────────────┘ │ └────────────┘ │ └──────────┘ │ └─────────────┘ │
│ │ │ │ │
└─────────────────┴────────────────┴──────────────┴─────────────────┘
Принципы определения границ:
// ПРАВИЛЬНО: отдельный сервис для каждого Bounded Context
// 1. User Service - управляет пользователями
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<User> createUser(@RequestBody CreateUserRequest req) {
// Вся логика управления пользователями тут
return ResponseEntity.ok(userService.create(req));
}
}
// 2. Product Service - управляет каталогом
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping
public List<Product> listProducts() {
// Вся логика каталога тут
return productService.list();
}
}
// 3. Order Service - управляет заказами
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest req) {
// Вся логика заказов тут
return ResponseEntity.ok(orderService.create(req));
}
}
2. Анализ зависимостей между модулями
Карта зависимостей
// ❌ ПЛОХО: циклические зависимости
// User Service зависит от Order Service
@Service
public class UserService {
@Autowired
private OrderService orderService; // зависимость
public List<Order> getUserOrders(String userId) {
return orderService.getOrdersByUserId(userId); // вызов
}
}
// Order Service зависит от User Service
@Service
public class OrderService {
@Autowired
private UserService userService; // обратная зависимость!
public Order createOrder(CreateOrderRequest req) {
User user = userService.getUser(req.getUserId()); // вызов
return orderRepository.save(new Order(user));
}
}
// Это создаёт проблемы при разделении на сервисы!
Решение: Service-to-Service Communication
// ✓ ПРАВИЛЬНО: асинхронная коммуникация через события
// User Service - не зависит от Order Service
@Service
public class UserService {
@Autowired
private EventPublisher eventPublisher;
public User createUser(CreateUserRequest req) {
User user = userRepository.save(new User(req));
// Издаём событие для других сервисов
eventPublisher.publish(new UserCreatedEvent(user.getId()));
return user;
}
}
// Order Service - слушает события User Service
@Service
public class OrderService {
@EventListener
public void onUserCreated(UserCreatedEvent event) {
// Инициализируем данные для нового пользователя
logger.info("User created, preparing order system: {}", event.getUserId());
}
public Order createOrder(CreateOrderRequest req) {
// Заказ знает юзера из события, не звоня в User Service
return orderRepository.save(new Order(req));
}
}
3. Управление данными и DATABASE PER SERVICE паттерн
❌ ПЛОХО: Одна БД для всех сервисов
services:
user-service:
database: shared_database # одна БД для всех!
product-service:
database: shared_database
order-service:
database: shared_database
# Проблемы:
# 1. Сервисы связаны через БД (tight coupling)
# 2. Изменение схемы БД влияет на все сервисы
# 3. Сложное масштабирование
# 4. Нет независимости в выборе БД
✓ ПРАВИЛЬНО: Separate Database Per Service
services:
user-service:
database: user_db # отдельная БД
db-type: PostgreSQL
product-service:
database: product_db # отдельная БД
db-type: MongoDB # может быть другой тип!
order-service:
database: order_db # отдельная БД
db-type: PostgreSQL
# Преимущества:
# 1. Слабая связность
# 2. Независимое масштабирование
# 3. Можем менять БД без влияния на другие сервисы
# 4. Оптимизация для каждого сервиса
// Каждый сервис управляет своей БД
// User Service - PostgreSQL
@Configuration
public class UserDataSourceConfig {
@Bean
public DataSource userDataSource() {
return DataSourceBuilder.create()
.driverClassName("org.postgresql.Driver")
.url("jdbc:postgresql://user-db:5432/users")
.username("user")
.password("password")
.build();
}
}
// Product Service - MongoDB
@Configuration
public class ProductDataSourceConfig {
@Bean
public MongoClient mongoClient() {
return MongoClients.create("mongodb://product-db:27017");
}
}
4. Синхронизация данных между сервисами
❌ Синхронные вызовы
// Order Service вызывает User Service синхронно
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
public Order createOrder(CreateOrderRequest req) {
// Синхронный вызов - блокирует поток
User user = restTemplate.getForObject(
"http://user-service/users/" + req.getUserId(),
User.class
);
if (user == null) {
throw new UserNotFoundException();
}
return orderRepository.save(new Order(user));
}
}
// Проблемы:
// 1. Если User Service упал - Order Service упадёт
// 2. Медленный отклик (ждём ответа от другого сервиса)
// 3. Сложная обработка ошибок
✓ Асинхронная коммуникация через события (Event-Driven)
// Order Service публикует событие
@Service
public class OrderService {
@Autowired
private EventPublisher eventPublisher;
@Autowired
private OrderRepository orderRepository;
public void createOrder(CreateOrderRequest req) {
Order order = new Order();
order.setUserId(req.getUserId());
order.setStatus(OrderStatus.PENDING);
order = orderRepository.save(order);
// Издаём событие - асинхронно
eventPublisher.publish(new OrderCreatedEvent(
order.getId(),
order.getUserId(),
order.getTotal()
));
}
}
// Notification Service слушает событие
@Service
public class NotificationService {
@KafkaListener(topics = "order-created")
public void onOrderCreated(OrderCreatedEvent event) {
// Отправи уведомление пользователю
emailService.send(
"user-" + event.getUserId() + "@example.com",
"Your order #" + event.getOrderId() + " is confirmed!"
);
}
}
// User Service может обновить статистику
@Service
public class UserStatsService {
@KafkaListener(topics = "order-created")
public void onOrderCreated(OrderCreatedEvent event) {
// Обновить счётчик заказов пользователя
userStatsRepository.incrementOrderCount(event.getUserId());
}
}
5. Распределённые транзакции (SAGA Pattern)
Проблема: ACID транзакции невозможны
Оригинальная операция в монолите:
BEGIN TRANSACTION
- Проверить баланс пользователя
- Зарезервировать товар (уменьшить inventory)
- Создать заказ
- Обработать платёж
COMMIT
Если что-то пошло не так - ROLLBACK всё
SAGA Pattern (Orchestration)
// Сагаа оркестрирует процесс
@Service
public class OrderSagaOrchestrator {
@Autowired
private UserService userService;
@Autowired
private InventoryService inventoryService;
@Autowired
private PaymentService paymentService;
@Autowired
private OrderRepository orderRepository;
public Order executeOrderSaga(CreateOrderRequest req) throws SagaException {
Order order = null;
try {
// Шаг 1: Проверка пользователя
User user = userService.getUser(req.getUserId());
if (user == null) {
throw new SagaException("User not found");
}
// Шаг 2: Зарезервировать товары
boolean reserved = inventoryService.reserve(
req.getProductId(),
req.getQuantity()
);
if (!reserved) {
throw new SagaException("Product not available");
}
// Шаг 3: Обработать платёж
boolean paid = paymentService.processPayment(
user.getId(),
req.getTotal()
);
if (!paid) {
// Компенсирующая транзакция: отмени резервирование
inventoryService.release(req.getProductId(), req.getQuantity());
throw new SagaException("Payment failed");
}
// Шаг 4: Создать заказ
order = orderRepository.save(new Order(user, req));
} catch (SagaException e) {
// Откати все успешные шаги
logger.error("Order saga failed: {}", e.getMessage());
if (order != null && order.getId() != null) {
orderRepository.delete(order);
}
throw e;
}
return order;
}
}
6. API Gateway паттерн
Вместо прямых вызовов сервисов
// ❌ ПЛОХО: Клиент вызывает сервисы напрямую
@RestController
public class ClientController {
@GetMapping("/profile/{userId}")
public ProfileData getProfile(@PathVariable String userId) {
// Клиент знает про существование User Service
User user = restTemplate.getForObject(
"http://user-service/users/" + userId,
User.class
);
// Клиент знает про существование Order Service
List<Order> orders = restTemplate.getForObject(
"http://order-service/users/" + userId + "/orders",
List.class
);
return new ProfileData(user, orders);
}
}
✓ API Gateway
// Клиент обращается только к Gateway
@RestController
public class ApiGatewayController {
@GetMapping("/api/profile/{userId}")
public ProfileData getProfile(@PathVariable String userId) {
// Gateway знает о всех сервисах и маршрутизирует
return profileService.getProfile(userId);
}
}
// Через spring-cloud-gateway
@SpringBootApplication
public class ApiGatewayApp {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r
.path("/users/**")
.uri("http://user-service:8001"))
.route("product-service", r -> r
.path("/products/**")
.uri("http://product-service:8002"))
.route("order-service", r -> r
.path("/orders/**")
.uri("http://order-service:8003"))
.build();
}
}
7. Сервис дискавери (Service Discovery)
# docker-compose с Consul
version: '3.8'
services:
consul:
image: consul:latest
ports:
- "8500:8500"
command: agent -server -bootstrap-expect=1 -ui -client=0.0.0.0
user-service:
image: user-service:latest
environment:
CONSUL_HOST: consul
SERVICE_NAME: user-service
order-service:
image: order-service:latest
environment:
CONSUL_HOST: consul
SERVICE_NAME: order-service
USER_SERVICE_URL: http://user-service:8001 # discovery URL
8. Логирование и трассировка (Distributed Tracing)
// Spring Cloud Sleuth + Zipkin
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
public Order createOrder(CreateOrderRequest req) {
// Sleuth автоматически добавляет trace ID
logger.info("Creating order for user: {}", req.getUserId());
User user = restTemplate.getForObject(
"http://user-service/users/" + req.getUserId(),
User.class
);
// Trace ID будет одинаковым в логах всех сервисов!
}
}
// В логах увидишь:
// user-service: [2024-03-22 10:15:30] [trace-id: abc123] User retrieved
// order-service: [2024-03-22 10:15:31] [trace-id: abc123] Creating order
// notification-service: [2024-03-22 10:15:32] [trace-id: abc123] Sending email
9. Мониторинг и Alerting
# Prometheus + Grafana
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
user-service:
image: user-service:latest
environment:
MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE: prometheus
10. Чеклист при разделении монолита
✅ Анализ и планирование:
- Определены Bounded Context с DDD
- Идентифицированы зависимости между модулями
- Нет циклических зависимостей
- Каждый сервис имеет одну ответственность (SRP)
✅ Архитектура данных:
- Database per service паттерн
- Данные не репликируются (Single source of truth)
- Решена проблема распределённых транзакций (SAGA)
✅ Коммуникация:
- Определены синхронные и асинхронные операции
- Есть API Gateway
- Есть Service Discovery
- Есть Circuit Breaker для отказоустойчивости
✅ Операции:
- Настроено логирование и трассировка
- Есть мониторинг и alerting
- Есть процесс развёртывания (CI/CD)
- Есть health checks для каждого сервиса
Итог
При разделении монолита обращай внимание на:
- DDD Bounded Contexts — правильные границы сервисов
- Слабая связность — минимальные зависимости
- Database per Service — независимые хранилища
- Event-Driven архитектура — асинхронная коммуникация
- API Gateway — единая точка входа
- Распределённые транзакции — SAGA паттерн
- Логирование и трассировка — видимость во всех сервисах
- Мониторинг — алерты и метрики
- Отказоустойчивость — Circuit Breaker, retries
- Развёртывание — контейнеризация и оркестрация (K8s)