senior
Как проектировать API для микросервисной архитектуры?
Проектирование API для микросервисов учитывает распределённую природу системы: сетевую ненадёжность, независимое развёртывание, согласованность данных и отказоустойчивость.
Синхронное взаимодействие (REST / gRPC)
Один сервис напрямую вызывает другой и ждёт ответа.
Пример вызова через RestClient
@Service
@RequiredArgsConstructor
public class OrderService {
private final RestClient userServiceClient;
public OrderDto createOrder(Long userId, CreateOrderRequest request) {
UserDto user = userServiceClient.get()
.uri("/api/users/{id}", userId)
.retrieve()
.body(UserDto.class);
if (user == null) {
throw new UserNotFoundException("User not found: " + userId);
}
Order order = orderMapper.toEntity(request);
order.setUserId(userId);
order.setUserEmail(user.email());
return orderMapper.toDto(orderRepository.save(order));
}
}
@Configuration
public class RestClientConfig {
@Bean
public RestClient userServiceClient(RestClient.Builder builder) {
return builder
.baseUrl("http://user-service:8080")
.defaultHeader("Content-Type", "application/json")
.requestInterceptor(new TokenRelayInterceptor())
.build();
}
}
Асинхронное взаимодействие (Events / Kafka)
Сервисы общаются через события — отправитель публикует событие, получатели подписаны на него.
Пример с Kafka
// Публикация события
@Service
@RequiredArgsConstructor
public class OrderService {
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
@Transactional
public OrderDto createOrder(CreateOrderRequest request) {
Order saved = orderRepository.save(orderMapper.toEntity(request));
OrderEvent event = new OrderEvent(
saved.getId(), "ORDER_CREATED",
saved.getUserId(), saved.getTotal(), Instant.now()
);
kafkaTemplate.send("order-events", saved.getId().toString(), event);
return orderMapper.toDto(saved);
}
}
// Обработка события в другом сервисе
@Service
@Slf4j
public class OrderEventListener {
@KafkaListener(topics = "order-events", groupId = "notification-service")
public void handleOrderEvent(OrderEvent event) {
if ("ORDER_CREATED".equals(event.eventType())) {
notificationService.sendOrderConfirmation(event.userId(), event.orderId());
}
}
}
API Composition — агрегация данных из нескольких сервисов
Пример с Virtual Threads (Java 21+)
@RestController
@RequestMapping("/api/customer-view")
@RequiredArgsConstructor
public class CustomerViewController {
private final RestClient userServiceClient;
private final RestClient orderServiceClient;
private final RestClient loyaltyServiceClient;
@GetMapping("/{userId}")
public ResponseEntity<CustomerView> getCustomerView(@PathVariable Long userId) {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var userTask = scope.fork(() ->
userServiceClient.get()
.uri("/api/users/{id}", userId)
.retrieve()
.body(UserDto.class));
var ordersTask = scope.fork(() ->
orderServiceClient.get()
.uri("/api/orders?userId={id}&limit=5", userId)
.retrieve()
.body(new ParameterizedTypeReference<List<OrderDto>>() {}));
var loyaltyTask = scope.fork(() ->
loyaltyServiceClient.get()
.uri("/api/loyalty/{id}", userId)
.retrieve()
.body(LoyaltyDto.class));
scope.join().throwIfFailed();
return ResponseEntity.ok(new CustomerView(
userTask.get(), ordersTask.get(), loyaltyTask.get()
));
}
}
}
Circuit Breaker — защита от каскадных сбоев
Пример с Resilience4j
@Service
@RequiredArgsConstructor
public class UserServiceClient {
private final RestClient restClient;
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
@Retry(name = "userService")
@TimeLimiter(name = "userService")
public UserDto getUser(Long id) {
return restClient.get()
.uri("/api/users/{id}", id)
.retrieve()
.body(UserDto.class);
}
private UserDto getUserFallback(Long id, Throwable ex) {
log.warn("User service unavailable, returning fallback for user {}", id, ex);
return new UserDto(id, "Unknown User", null);
}
}
resilience4j:
circuitbreaker:
instances:
userService:
sliding-window-size: 10
failure-rate-threshold: 50
wait-duration-in-open-state: 10s
retry:
instances:
userService:
max-attempts: 3
wait-duration: 500ms
exponential-backoff-multiplier: 2
timelimiter:
instances:
userService:
timeout-duration: 3s
Saga Pattern — распределённые транзакции
Saga — последовательность локальных транзакций, где каждый шаг имеет компенсирующее действие для отката.
| Характеристика | Хореография (события) | Оркестрация (координатор) |
|---|---|---|
| Связанность | Слабая (через события) | Средняя (координатор знает все шаги) |
| Простота | Простые Saga | Сложные Saga с множеством шагов |
| Отслеживание | Сложно (события распределены) | Просто (всё в координаторе) |
| Единая точка отказа | Нет | Да (координатор) |
Примеры Saga
Хореографическая Saga (через события):
@Service
@RequiredArgsConstructor
public class PaymentEventListener {
private final PaymentService paymentService;
private final KafkaTemplate<String, PaymentEvent> kafkaTemplate;
@KafkaListener(topics = "order-events", groupId = "payment-service")
public void handleOrderCreated(OrderEvent event) {
if (!"ORDER_CREATED".equals(event.eventType())) return;
try {
paymentService.processPayment(event.orderId(), event.total());
kafkaTemplate.send("payment-events",
new PaymentEvent(event.orderId(), "PAYMENT_COMPLETED"));
} catch (InsufficientFundsException e) {
kafkaTemplate.send("payment-events",
new PaymentEvent(event.orderId(), "PAYMENT_FAILED"));
}
}
}
Оркестрационная Saga:
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderSagaOrchestrator {
private final PaymentServiceClient paymentClient;
private final StockServiceClient stockClient;
private final DeliveryServiceClient deliveryClient;
@Transactional
public OrderResult executeOrderSaga(Order order) {
try {
StockReservation reservation = stockClient.reserve(order.getItems());
PaymentResult payment;
try {
payment = paymentClient.charge(order.getUserId(), order.getTotal());
} catch (Exception e) {
stockClient.cancelReservation(reservation.getId());
throw e;
}
try {
deliveryClient.schedule(order.getId(), order.getDeliveryAddress());
} catch (Exception e) {
paymentClient.refund(payment.getId());
stockClient.cancelReservation(reservation.getId());
throw e;
}
order.setStatus(OrderStatus.CONFIRMED);
return OrderResult.success(order);
} catch (Exception e) {
log.error("Order saga failed for order {}", order.getId(), e);
order.setStatus(OrderStatus.FAILED);
return OrderResult.failure(order, e.getMessage());
}
}
}
Ключевые принципы
- Каждый микросервис владеет своими данными (database per service).
- Синхронная связь (REST/gRPC) — для немедленного ответа. Асинхронная (Kafka) — для событий.
- Circuit Breaker обязателен для всех синхронных вызовов.
- Saga Pattern заменяет распределённые транзакции.
- Все обработчики событий должны быть идемпотентными.
Частые ошибки
- Общая база данных для нескольких сервисов — нарушает автономность.
- Отсутствие Circuit Breaker — каскадный сбой всей системы.
- Синхронные цепочки вызовов (A -> B -> C -> D) — увеличивают латентность.
- Распределённые транзакции (2PC) — не масштабируются, используйте Saga.
- Слишком мелкие сервисы (nano-services) — overhead превышает пользу.
Как используется в 2026
- Spring Boot 3.x + Virtual Threads — синхронный код с производительностью реактивного.
- Spring Modulith — модульный монолит как альтернатива для проектов среднего размера.
- Testcontainers — стандарт для интеграционного тестирования.
- OpenTelemetry — единый стандарт для distributed tracing, metrics и logging.
- Service Mesh (Istio, Linkerd) берёт на себя retry, circuit breaker и mTLS.
На собеседовании: нужно показать знание обоих типов взаимодействия (синхронное и асинхронное), Circuit Breaker и Saga Pattern. Частая ошибка — не упомянуть принцип database per service или предлагать 2PC вместо Saga для распределённых транзакций.