Структурные паттерны микросервисов

API Gateway, BFF, Sidecar, Service Mesh, Strangler Fig, ACL, Service Discovery — как организовать взаимодействие сервисов. Примеры на Spring Cloud Gateway.

Структурные паттерны микросервисов

Все примеры в статье — на сквозном кейсе сайта: высоконагруженный маркетплейс. Эта статья описывает, как сервисы взаимодействуют друг с другом, как маршрутизируется трафик и как организуется доступ клиентов к backend-системе. Каждый паттерн сопровождается описанием проблемы, решением, схемой и примером кода на Spring Boot / Spring Cloud Gateway.

1. API Gateway

Проблема

В микросервисной архитектуре десятки сервисов, каждый со своим адресом, портом и протоколом аутентификации. Если клиент обращается к ним напрямую, он должен знать адреса всех сервисов, самостоятельно обрабатывать аутентификацию для каждого, а при добавлении нового сервиса — обновлять код клиента.

Дополнительные сквозные задачи — rate limiting, логирование, CORS, SSL termination — приходится реализовывать в каждом сервисе отдельно.

Решение

Единая точка входа (API Gateway) принимает все клиентские запросы и маршрутизирует их к нужным сервисам. Gateway берёт на себя сквозные задачи: аутентификацию, авторизацию, rate limiting, логирование, трансформацию запросов.

diagram

Пример

# application.yml — Spring Cloud Gateway
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20

        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/users/**
          filters:
            - StripPrefix=1
// GlobalAuthFilter.java — сквозная аутентификация на уровне Gateway
@Component
public class GlobalAuthFilter implements GlobalFilter, Ordered {

    private final JwtDecoder jwtDecoder;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getHeaders()
            .getFirst(HttpHeaders.AUTHORIZATION);

        if (token == null || !token.startsWith("Bearer ")) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        try {
            Jwt jwt = jwtDecoder.decode(token.substring(7));
            // Передаём данные пользователя downstream-сервисам
            ServerHttpRequest request = exchange.getRequest().mutate()
                .header("X-User-Id", jwt.getSubject())
                .header("X-User-Roles", String.join(",", jwt.getClaimAsStringList("roles")))
                .build();
            return chain.filter(exchange.mutate().request(request).build());
        } catch (JwtException e) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
    }

    @Override
    public int getOrder() { return -1; }
}

Когда использовать

  • Много микросервисов и нужна единая точка входа
  • Сквозные задачи (auth, logging, rate limiting) дублируются в сервисах
  • Нужно скрыть внутреннюю топологию от клиентов

Когда не использовать

  • Монолитное приложение — Gateway добавляет ненужный hop
  • Один-два сервиса — проще настроить всё напрямую

2. Gateway Routing

Проблема

API Gateway должен направлять запрос к конкретному сервису на основании URL, HTTP-метода, заголовков или других признаков. Без явной маршрутизации Gateway становится «чёрным ящиком», в котором невозможно понять, куда уходит каждый запрос.

Решение

Gateway Routing — паттерн, при котором Gateway маршрутизирует каждый запрос к одному конкретному backend-сервису на основании набора предикатов (predicates). В отличие от Gateway Aggregation, здесь нет объединения ответов — один запрос всегда идёт к одному сервису.

Пример

Маршрутизация может быть основана на разных критериях:

spring:
  cloud:
    gateway:
      routes:
        # По пути
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**

        # По пути + HTTP-методу
        - id: payment-service-write
          uri: lb://payment-service
          predicates:
            - Path=/api/payments/**
            - Method=POST,PUT,DELETE

        # По заголовку (версионирование API)
        - id: user-service-v2
          uri: lb://user-service-v2
          predicates:
            - Path=/api/users/**
            - Header=X-API-Version, v2

        - id: user-service-v1
          uri: lb://user-service-v1
          predicates:
            - Path=/api/users/**

        # По query-параметру
        - id: search-service
          uri: lb://search-service
          predicates:
            - Path=/api/products/**
            - Query=search

        # По времени (canary deployment)
        - id: new-catalog-service
          uri: lb://catalog-service-v2
          predicates:
            - Path=/api/catalog/**
            - Weight=catalog-group, 20   # 20% трафика
        - id: old-catalog-service
          uri: lb://catalog-service-v1
          predicates:
            - Path=/api/catalog/**
            - Weight=catalog-group, 80   # 80% трафика

Помимо маршрутизации, Gateway может модифицировать запрос на лету:

        - id: legacy-adapter
          uri: lb://new-service
          predicates:
            - Path=/old-api/v1/**
          filters:
            - StripPrefix=2                           # /old-api/v1/orders → /orders
            - AddRequestHeader=X-Source, legacy        # пометка источника
            - RewritePath=/old-api/v1/(?<path>.*), /api/${path}

Когда использовать

  • Нужно скрыть адреса внутренних сервисов за единым URL
  • Разные версии API обслуживаются разными сервисами
  • Canary-деплой: часть трафика направляется на новую версию

3. Gateway Aggregation

Проблема

Клиенту нужны данные из нескольких сервисов одновременно. Например, страница заказа требует данные из Order Service, User Service и Delivery Service. Если клиент делает три отдельных запроса — это три round-trip по сети, что медленно на мобильных устройствах с высоким latency.

Решение

Gateway принимает один запрос от клиента, параллельно вызывает несколько backend-сервисов и собирает ответы в единый агрегированный ответ.

diagram

Пример

@RestController
@RequiredArgsConstructor
public class OrderDetailsAggregationController {

    private final WebClient.Builder webClientBuilder;

    @GetMapping("/api/order-details/{orderId}")
    public Mono<OrderDetailsResponse> getOrderDetails(@PathVariable Long orderId) {

        Mono<OrderDto> orderMono = webClientBuilder.build()
            .get()
            .uri("http://order-service/orders/{id}", orderId)
            .retrieve()
            .bodyToMono(OrderDto.class)
            .onErrorResume(e -> Mono.empty());   // partial failure

        Mono<UserDto> userMono = webClientBuilder.build()
            .get()
            .uri("http://user-service/users/{id}", getUserIdFromOrder(orderId))
            .retrieve()
            .bodyToMono(UserDto.class)
            .onErrorResume(e -> Mono.empty());

        Mono<DeliveryDto> deliveryMono = webClientBuilder.build()
            .get()
            .uri("http://delivery-service/deliveries?orderId={id}", orderId)
            .retrieve()
            .bodyToMono(DeliveryDto.class)
            .onErrorResume(e -> Mono.empty());

        // Собираем параллельно
        return Mono.zip(orderMono, userMono, deliveryMono)
            .map(tuple -> new OrderDetailsResponse(tuple.getT1(), tuple.getT2(), tuple.getT3()));
    }
}

Отличие от BFF

Gateway Aggregation — это агрегация на уровне инфраструктуры (API Gateway). Она не адаптирует данные под конкретного клиента, а просто собирает ответы в один.

BFF — это отдельный сервис, который дополнительно трансформирует и фильтрует данные под нужды конкретного типа клиента.

Когда использовать

  • Клиенту нужны данные из 2-3 сервисов, и агрегация тривиальна
  • Снизить количество round-trip для клиентов с высоким latency (мобильные)

Когда не использовать

  • Нужна сложная трансформация данных — это задача BFF
  • Агрегация требует бизнес-логики (вычисления, фильтрация по правилам)
  • Больше 3-4 сервисов — Gateway становится «толстым»

4. Backend for Frontend (BFF)

Проблема

У приложения несколько типов клиентов: мобильное приложение, SPA-админка, публичный сайт. Каждому нужны разные данные, разные форматы ответов и разные модели безопасности.

Мобильному приложению нужен минимум данных (экономия трафика), веб-админке — полный набор с правами доступа, публичному сайту — только публичные данные. Один API не может эффективно обслуживать все эти нужды.

Решение

Для каждого типа клиента создаётся отдельный BFF-сервис. BFF не содержит бизнес-логики — он вызывает доменные сервисы и адаптирует ответы под своего клиента.

diagram

Пример

// Mobile BFF — минимум данных, stateless, Bearer Token
@RestController
@RequiredArgsConstructor
public class MobileOrderController {

    private final OrderServiceClient orderClient;
    private final UserServiceClient userClient;

    @GetMapping("/api/orders/{id}")
    public MobileOrderResponse getOrder(@PathVariable Long id) {
        OrderDto order = orderClient.getOrder(id);
        UserDto user = userClient.getUser(order.getUserId());

        // Адаптация для мобильного: только нужные поля
        return MobileOrderResponse.builder()
            .orderId(order.getId())
            .status(order.getStatus())
            .totalAmount(order.getTotalAmount())
            .userName(user.getFirstName())
            .build();
    }
}

// Web BFF — полный набор данных, stateful сессии, OAuth2
@RestController
@RequiredArgsConstructor
public class AdminOrderController {

    private final OrderServiceClient orderClient;
    private final UserServiceClient userClient;
    private final PaymentServiceClient paymentClient;
    private final AuditServiceClient auditClient;

    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/api/admin/orders/{id}")
    public AdminOrderResponse getOrder(@PathVariable Long id) {
        OrderDto order = orderClient.getOrder(id);
        UserDto user = userClient.getUser(order.getUserId());
        PaymentDto payment = paymentClient.getPayment(order.getPaymentId());
        List<AuditLogDto> auditLog = auditClient.getLogsForOrder(id);

        return AdminOrderResponse.builder()
            .orderId(order.getId())
            .status(order.getStatus())
            .totalAmount(order.getTotalAmount())
            .user(AdminUserDto.from(user))
            .payment(AdminPaymentDto.from(payment))
            .auditLog(auditLog)
            .internalNotes(order.getInternalNotes())
            .build();
    }
}

Отличие от API Gateway

  • API Gateway — инфраструктурный компонент. Маршрутизирует запросы и решает сквозные задачи.
  • BFF — прикладной сервис. Знает про бизнес-контекст своего клиента и адаптирует данные:
    • выбирает, какие поля возвращать;
    • агрегирует данные с учётом потребностей клиента;
    • реализует модель безопасности (stateless для мобильных, сессии для веба).
diagram

Когда использовать

  • Больше одного типа клиента с разными потребностями
  • Клиентам нужны разные подмножества данных или разные форматы
  • Разные модели безопасности (stateless vs stateful, JWT vs sessions)

Когда не использовать

  • Один тип клиента — достаточно API Gateway
  • Все клиенты потребляют одинаковые данные

5. Gateway Offloading

Проблема

Каждый микросервис самостоятельно реализует SSL termination, аутентификацию, сжатие ответов, CORS, IP whitelisting. Это дублирование кода, усложнение конфигурации и потенциальные расхождения в реализации.

Решение

Сквозные задачи, не связанные с бизнес-логикой, выносятся на уровень Gateway. Внутренние сервисы работают по простому HTTP без SSL и без аутентификации — это делает Gateway.

Пример

server:
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-type: PKCS12
  compression:
    enabled: true
    mime-types: application/json,text/html
    min-response-size: 1024

spring:
  cloud:
    gateway:
      default-filters:
        # Аутентификация — один раз для всех маршрутов
        - name: JwtAuthFilter

        # Сжатие ответов
        - name: ModifyResponseBody
          args:
            rewriteFunction: GzipRewriteFunction

        # Rate limiting по IP
        - name: RequestRateLimiter
          args:
            key-resolver: "#{@ipKeyResolver}"
            redis-rate-limiter.replenishRate: 100
            redis-rate-limiter.burstCapacity: 200

        # Стандартные security headers
        - AddResponseHeader=X-Content-Type-Options, nosniff
        - AddResponseHeader=X-Frame-Options, DENY
        - AddResponseHeader=Strict-Transport-Security, max-age=31536000

После offloading внутренний сервис становится проще:

@RestController
public class OrderController {

    @GetMapping("/orders/{id}")
    public OrderDto getOrder(
            @PathVariable Long id,
            @RequestHeader("X-User-Id") String userId) {
        // Gateway уже проверил JWT и передал userId в заголовке
        return orderService.getOrder(id, userId);
    }
}

Что выносить на Gateway, а что оставить в сервисе

Выносить на Gateway:

  • SSL termination
  • JWT-валидация (проверка подписи, срок действия)
  • Rate limiting
  • CORS
  • Сжатие ответов
  • Security headers

Оставить в сервисе:

  • Авторизация (проверка прав на конкретный ресурс) — Gateway не знает бизнес-логику
  • Валидация тела запроса — зависит от доменной модели

6. Sidecar

Проблема

Некоторые задачи одинаковы для всех сервисов: сбор метрик, логирование, шифрование трафика, service discovery. Реализовывать их в каждом сервисе — дублирование. А если сервисы написаны на разных языках (Java, Go, Python) — нужно писать одну и ту же логику на трёх языках.

Решение

Рядом с каждым сервисом деплоится вспомогательный процесс (sidecar), который берёт на себя инфраструктурные задачи. Sidecar и основной сервис живут в одном поде (Kubernetes) и общаются через localhost.

diagram

Пример

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  template:
    spec:
      containers:
        # Основной сервис
        - name: order-service
          image: order-service:1.0
          ports:
            - containerPort: 8080

        # Sidecar — Envoy Proxy
        - name: envoy-sidecar
          image: envoyproxy/envoy:v1.28
          ports:
            - containerPort: 9901   # admin
            - containerPort: 15001  # inbound
            - containerPort: 15006  # outbound
          volumeMounts:
            - name: envoy-config
              mountPath: /etc/envoy

      volumes:
        - name: envoy-config
          configMap:
            name: order-service-envoy-config

Когда использовать

  • Полиглотный стек (сервисы на разных языках)
  • Нужна единообразная обработка сетевого трафика (mTLS, retry, circuit breaker)
  • Инфраструктурные задачи не должны зависеть от языка/фреймворка

Когда не использовать

  • Все сервисы на одном языке/фреймворке — проще библиотеки (Resilience4j)
  • Sidecar добавляет latency и потребляет ресурсы

7. Service Mesh

Проблема

С ростом количества сервисов управление сетевым взаимодействием усложняется экспоненциально. Нужно контролировать: кто с кем может общаться (network policies), шифрование трафика (mTLS), retry и timeout для каждой пары сервисов, canary deployment, observability.

Паттерн Sidecar решает это для одного сервиса, но кто управляет всеми сайдкарами?

Решение

Service Mesh — инфраструктурный слой, который управляет всей межсервисной коммуникацией. Состоит из:

  • Data Plane — сайдкар-прокси (Envoy) в каждом поде, через которые проходит весь трафик
  • Control Plane — управляющий компонент (Istio, Linkerd), который конфигурирует все прокси
diagram

Пример

# Istio VirtualService — canary deployment (20% трафика на v2)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: order-service
spec:
  hosts:
    - order-service
  http:
    - route:
        - destination:
            host: order-service
            subset: v1
          weight: 80
        - destination:
            host: order-service
            subset: v2
          weight: 20
      retries:
        attempts: 3
        perTryTimeout: 2s
        retryOn: 5xx,reset,connect-failure
      timeout: 10s

# Istio DestinationRule — circuit breaker
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: payment-service
spec:
  host: payment-service
  trafficPolicy:
    outlierDetection:
      consecutive5xxErrors: 3
      interval: 30s
      baseEjectionTime: 60s
      maxEjectionPercent: 50

# Istio PeerAuthentication — mTLS для всего namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT

Service Mesh vs API Gateway

  • API Gateway обрабатывает north-south трафик (клиент → система).
  • Service Mesh управляет east-west трафиком (сервис → сервис).
diagram

Когда использовать

  • Десятки сервисов с активной межсервисной коммуникацией
  • Строгие требования к безопасности (mTLS everywhere)
  • Нужен canary deployment и fine-grained traffic management
  • Полиглотный стек

Когда не использовать

  • Меньше 10 сервисов — overhead перевесит выгоду
  • Команда не готова к операционной сложности (Istio непрост в поддержке)
  • Достаточно библиотечного подхода (Spring Cloud, Resilience4j)

8. Strangler Fig

Проблема

Есть работающий монолит, который нужно перевести на микросервисы. Переписать всё с нуля — рискованно и долго. Остановить разработку новых фич на время миграции — бизнес не позволит.

Решение

Новая функциональность реализуется в микросервисах. Старая — постепенно «задушивается»: трафик переключается с монолита на новые сервисы по мере их готовности. Монолит продолжает работать для функций, которые ещё не мигрированы.

Название — по аналогии с фикусом-душителем (Strangler Fig), который обвивает дерево и постепенно замещает его.

Пример

# application.yml — Gateway на этапе миграции
spring:
  cloud:
    gateway:
      routes:
        # Новый Order Service — мигрированная функциональность
        - id: new-order-service
          uri: http://order-service:8080
          predicates:
            - Path=/api/orders/**
          filters:
            - StripPrefix=1

        # Новый User Service — мигрированная функциональность
        - id: new-user-service
          uri: http://user-service:8080
          predicates:
            - Path=/api/users/**
          filters:
            - StripPrefix=1

        # Монолит — всё остальное (ещё не мигрировано)
        - id: legacy-monolith
          uri: http://monolith:8080
          predicates:
            - Path=/api/**
          order: 9999   # самый низкий приоритет

Постепенная миграция с feature-flag:

@Component
@RequiredArgsConstructor
public class OrderFacade {

    private final LegacyOrderService legacyService;
    private final NewOrderServiceClient newServiceClient;

    @Value("${feature.use-new-order-service:false}")
    private boolean useNewService;

    public OrderDto getOrder(Long id) {
        if (useNewService) {
            return newServiceClient.getOrder(id);
        }
        return legacyService.getOrder(id);
    }
}

Этапы миграции

  1. Идентификация — выбираем модуль монолита для миграции (начинаем с наименее связанного)
  2. Реализация — создаём микросервис с тем же API
  3. Перенаправление — Gateway отправляет трафик на новый сервис
  4. Верификация — сравниваем ответы старого и нового сервиса (shadow traffic)
  5. Удаление — вырезаем код из монолита

Когда использовать

  • Миграция работающего монолита на микросервисы
  • Нельзя остановить разработку на время миграции
  • Нужна возможность откатиться (переключить трафик обратно на монолит)

Когда не использовать

  • Greenfield-проект — сразу делайте микросервисы
  • Монолит работает хорошо и не мешает — не мигрируйте ради моды

9. Anti-Corruption Layer (ACL)

Проблема

Микросервис интегрируется с внешней или legacy-системой, у которой своя доменная модель, свои форматы данных, свои соглашения об именах. Если напрямую использовать модели внешней системы в своём коде — доменная логика засоряется чужими абстракциями. Смена внешней системы потребует переписывания всего сервиса.

Решение

Между сервисом и внешней системой ставится слой-переводчик (Anti-Corruption Layer), который преобразует модели внешней системы в доменные модели сервиса и обратно.

diagram

Пример

// Модель внешней системы — «чужие» имена и структура
public class ExternalPaymentResponse {
    private String txn_id;
    private int amount_cents;
    private String ccy;
    private String sts;          // "S" = success, "F" = failed, "P" = pending
    private long created_ts;
}

// Наша доменная модель — чистые, понятные имена
public record Payment(
    UUID transactionId,
    Money amount,
    PaymentStatus status,
    OffsetDateTime createdAt
) {}

public record Money(BigDecimal value, Currency currency) {}

public enum PaymentStatus { PENDING, SUCCESS, FAILED }

// ACL — адаптер, изолирующий внешнюю модель
@Component
@RequiredArgsConstructor
public class PaymentGatewayAdapter {

    private final ExternalPaymentClient externalClient;
    private final PaymentMapper mapper;

    public Payment getPayment(UUID transactionId) {
        ExternalPaymentResponse external =
            externalClient.getTransaction(transactionId.toString());
        return mapper.toDomain(external);
    }

    public Payment createPayment(Money amount, String description) {
        ExternalCreatePaymentRequest request = mapper.toExternal(amount, description);
        ExternalPaymentResponse response = externalClient.create(request);
        return mapper.toDomain(response);
    }
}

@Component
public class PaymentMapper {

    public Payment toDomain(ExternalPaymentResponse external) {
        return new Payment(
            UUID.fromString(external.getTxnId()),
            new Money(
                BigDecimal.valueOf(external.getAmountCents(), 2),  // центы → рубли
                Currency.fromCode(external.getCcy())
            ),
            mapStatus(external.getSts()),
            Instant.ofEpochSecond(external.getCreatedTs())
                .atOffset(ZoneOffset.UTC)
        );
    }

    private PaymentStatus mapStatus(String externalStatus) {
        return switch (externalStatus) {
            case "S" -> PaymentStatus.SUCCESS;
            case "F" -> PaymentStatus.FAILED;
            case "P" -> PaymentStatus.PENDING;
            default -> throw new IllegalArgumentException(
                "Unknown payment status: " + externalStatus);
        };
    }
}

Когда использовать

  • Интеграция с legacy-системой или внешним API с «чужой» доменной моделью
  • Планируется замена внешней системы в будущем
  • Доменная логика сервиса не должна зависеть от формата внешнего API

Когда не использовать

  • Внешняя система использует стандартный формат, совпадающий с вашей моделью
  • Простая интеграция без бизнес-логики (прокси без трансформации)

10. Service Registry & Discovery

Проблема

В микросервисной архитектуре сервисы масштабируются динамически — сегодня 3 инстанса Order Service, завтра 10. Инстансы приходят и уходят: деплои, автоскейлинг, падения. Хардкодить адреса невозможно.

Решение

Service Registry — реестр, в котором каждый сервис регистрируется при старте и отменяет регистрацию при остановке. Другие сервисы узнают адреса через этот реестр.

Два подхода:

  • Client-Side Discovery — клиент сам запрашивает реестр и выбирает инстанс (Eureka, Consul)
  • Server-Side Discovery — запрос проходит через балансировщик, который знает про реестр (Kubernetes Services, AWS ALB)
diagram diagram

Пример: Client-Side Discovery (Spring Cloud Eureka)

eureka:
  client:
    service-url:
      defaultZone: http://eureka-server:8761/eureka
  instance:
    prefer-ip-address: true
    lease-renewal-interval-in-seconds: 10
    lease-expiration-duration-in-seconds: 30
@FeignClient(name = "payment-service")  // Eureka resolves by name
public interface PaymentServiceClient {

    @PostMapping("/payments")
    PaymentDto createPayment(@RequestBody CreatePaymentRequest request);
}

Пример: Server-Side Discovery (Kubernetes)

В Kubernetes service discovery встроено — каждый Service получает DNS-имя:

apiVersion: v1
kind: Service
metadata:
  name: payment-service
spec:
  selector:
    app: payment-service
  ports:
    - port: 8080
payment-service:
  url: http://payment-service:8080

Kubernetes DNS автоматически резолвит payment-service в IP одного из Pod-ов. Не нужен отдельный реестр, Eureka или Consul.

Когда использовать

  • Eureka / Consul — если вы не в Kubernetes или нужны дополнительные возможности (health checks, KV store)
  • Kubernetes DNS — если вы в Kubernetes (встроенный, бесплатный, ничего настраивать не нужно)

Карта паттернов

diagram

Начните с простого

Если вы начинаете новый проект на микросервисах:

  1. API Gateway + Gateway Routing — единая точка входа, маршрутизация по путям
  2. Service Discovery — Kubernetes DNS, если вы в K8s
  3. Gateway Offloading — вынесите SSL и auth на Gateway

По мере роста:

  • BFF — когда появится второй тип клиента
  • Gateway Aggregation — когда клиенту нужны данные из нескольких сервисов
  • Anti-Corruption Layer — при интеграции с legacy

Для больших систем (50+ сервисов):

  • Service Mesh — когда управление east-west трафиком становится сложным
  • Strangler Fig — когда нужно мигрировать монолит

Ссылки