Сегмент services - как проектировать services-segment в микросервисной архитектуре

19 февраля 2026
Автор

Олег Марков

Введение

Сегмент services (далее будем называть его services-segment) — это логический слой или часть системы, где живет бизнес-логика и прикладные микросервисы. Когда вы проектируете распределенную систему, вам важно понимать, как выделить этот сегмент, что в него входит, как сервисы общаются друг с другом и через какие контракты.

Смотрите, здесь мы разберем services-segment как отдельный уровень архитектуры:

  • что обычно входит в сегмент services;
  • как разделять ответственность между сервисами;
  • как организовать взаимодействие;
  • какие типовые паттерны применяются;
  • как выглядит пример реализации простого services-segment;
  • как связать services-segment с другими слоями (например, API-gateway, data-segment).

Я буду приводить примеры на условном стеке (REST, HTTP, JSON, иногда gRPC), но сами принципы можно использовать в любом языке и технологиях.

Что такое services-segment и зачем он нужен

Логика сегмента services

Services-segment — это слой, в котором:

  • Инкапсулируется бизнес-логика.
  • Сервисы реализуют свои публичные контракты.
  • Выполняется оркестрация данных между клиентами и хранилищами.
  • Применяются доменные инварианты и правила.

Обычно services-segment отделен:

  • сверху — API-шлюзом, фронтендами, внешними интеграциями;
  • снизу — сегментом данных (БД, кэши, очереди);
  • по бокам — другими доменными сервисами.

Принцип границ ответственности

Давайте сразу зафиксируем ключевую идею: каждый сервис должен отвечать за четко очерченную область. Это может быть:

  • доменная область (users-service, billing-service, orders-service);
  • техническая функция (notification-service, mail-service, file-service);
  • интеграция (payment-gateway-service).

Если границы размыты, вы быстро получите «расползание» логики, дублирование и циклические зависимости.

Типы сервисов внутри services-segment

Внутри services-segment вы чаще всего увидите такие типы:

  • Доменные сервисы — реализуют бизнес-правила (order-service, catalog-service).
  • Инфраструктурные сервисы — обертки над внешними системами (sms-service, payment-service).
  • Оркестраторы / фасады — собирают цепочки операций из нескольких внутренних и внешних сервисов.

Хорошая практика — держать «ядро» домена в доменных сервисах и минимизировать бизнес-логику в инфраструктурных.

Архитектура services-segment

Слой сервисов в общей схеме

Представим упрощенную схему:

  • Client (web, mobile, интеграции) → API Gateway / BFF → services-segment (наши микросервисы) → data-segment (БД, очереди, кэши, файловые хранилища).

Сегмент services:

  • предоставляет API для gateway;
  • общается с другими сервисами по внутренним протоколам (REST, gRPC, события);
  • скрывает от клиентов детали хранения данных и внутренней структуры.

Варианты организации services-segment

В реальных системах services-segment может выглядеть по-разному:

  1. Один монолитный backend, логически разделенный на модули services.
  2. Набор независимых сервисов, развернутых отдельно (микросервисная архитектура).
  3. Гибрид, где есть несколько крупных сервисов (модульный монолит + микросервисы).

В статье дальше мы будем считать, что у вас набор отдельных микросервисов, но подходы легко переносятся на модульный монолит.

Проектирование сервисов внутри services-segment

Шаг 1. Определение доменов и bounded context

Давайте начнем с основной идеи: сначала определяются доменные области (bounded context), а затем под них выделяются сервисы.

Пример для e-commerce:

  • Пользователи и аутентификация (auth, identity).
  • Каталог товаров.
  • Корзина.
  • Заказы.
  • Оплаты.
  • Доставка.

Каждая область — кандидат на отдельный сервис или группу сервисов. Здесь важно:

  • не дробить слишком рано;
  • не складывать все в один «mega-service».

Шаг 2. Правила выделения сервисов

Обратите внимание на такие признаки, что нужен отдельный сервис:

  • У области свой словарь и модель (User, Customer, Account могут быть разными сущностями в разных контекстах).
  • Разные требования по масштабированию (catalog-service читает много, orders-service пишет меньше, но с транзакциями).
  • Разные требования по доступности и SLA.
  • Разные команды разработки.

Если вы чувствуете, что сервис начинает отвечать «за все подряд», — значит границы выбраны неправильно, попробуйте пересмотреть разбиение.

Шаг 3. Виды интерфейсов сервисов

Внутри services-segment сервисы предлагают:

  • синхронные интерфейсы:
    • HTTP REST;
    • gRPC;
  • асинхронные:
    • события в брокере (Kafka, RabbitMQ, NATS);
    • очередь задач.

Комбинация:

  • внешним клиентам — REST;
  • внутренним сервисам — gRPC и события.

Так вы уменьшаете задержки и повышаете устойчивость.

Контракты и модели данных в services-segment

API-контракты

Контракт сервиса — это описание того, что он делает и как с ним взаимодействовать:

  • endpoint-ы (URL, методы);
  • форматы запросов/ответов;
  • коды ошибок;
  • правила валидации.

Смотрите, я покажу вам простой пример контракта для orders-service в стиле OpenAPI (yaml):

paths:
  /orders:
    post:
      summary: Create new order
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
      responses:
        '201':
          description: Order created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderResponse'
        '400':
          description: Validation error

Комментарии:

  • Здесь мы явно описываем входные и выходные модели.
  • Этот контракт будет основой для генерации клиентских SDK.
  • Изменение контракта — изменение поведения сервиса, и его нужно версионировать.

Внутренние доменные модели

Важно разделять:

  • внешние DTO (Data Transfer Objects) — то, что идет по сети;
  • внутренние доменные модели — то, как сервис представляет объекты внутри.

Например, для заказа можно иметь:

  • OrderDTO — то, что уходит наружу (минимум полей, стабильная структура);
  • Order — доменная сущность с полями, нужными только внутри сервиса.

Давайте посмотрим пример кода на условном языке (похожем на Go):

// OrderDTO - модель, которая возвращается клиенту через API
type OrderDTO struct {
    ID        string    `json:"id"`         // Публичный идентификатор заказа
    Status    string    `json:"status"`     // Текущий статус
    Total     float64   `json:"total"`      // Итоговая сумма
    CreatedAt time.Time `json:"createdAt"`  // Дата создания
}

// Order - внутренняя доменная модель
type Order struct {
    ID           string        // Внутренний ID
    UserID       string        // Владелец заказа
    Items        []OrderItem   // Позиции заказа
    Total        float64       // Итоговая сумма
    Status       OrderStatus   // Статус, как enum
    Version      int           // Версия для оптимистичной блокировки
    CreatedAt    time.Time     // Дата создания
    UpdatedAt    time.Time     // Дата обновления
    PaymentState PaymentState  // Состояние оплаты
}

Комментарии к примеру:

  • В DTO мы не показываем все внутренние поля.
  • Доменная модель богаче и позволяет реализовывать бизнес-правила.
  • Такое разделение помогает менять внутреннюю реализацию без ломки внешних контрактов.

Взаимодействие сервисов внутри services-segment

Синхронные вызовы

Самый понятный вариант: один сервис вызывает HTTP/gRPC метод другого и ждет ответ.

Плюсы:

  • просто реализовать;
  • легко отлаживать;
  • привычно для большинства команд.

Минусы:

  • жесткая связность по доступности (если один сервис упал — другие ждут);
  • риск каскадных сбоев.

Давайте разберемся на примере:

  • order-service создает заказ;
  • для расчета стоимости доставки обращается к delivery-service.
// DeliveryClient - клиент для вызова delivery-service
type DeliveryClient interface {
    Calculate(ctx context.Context, req DeliveryCalcRequest) (DeliveryCalcResponse, error)
}

// OrderService - доменный сервис работы с заказами
type OrderService struct {
    deliveryClient DeliveryClient // Зависимость от delivery-service
}

// CreateOrder - пример метода создания заказа
func (s *OrderService) CreateOrder(ctx context.Context, cmd CreateOrderCommand) (*Order, error) {
    // Сначала валидируем входные данные
    if err := validateCreateOrder(cmd); err != nil {
        return nil, err // Возвращаем ошибку валидации
    }

    // Вызываем delivery-service для расчета стоимости доставки
    deliveryResp, err := s.deliveryClient.Calculate(ctx, DeliveryCalcRequest{
        Destination: cmd.Destination,
        Items:       cmd.Items,
    })
    if err != nil {
        return nil, fmt.Errorf("delivery calc failed: %w", err) // Оборачиваем ошибку
    }

    // Создаем доменную модель заказа с учетом результатов доставки
    order := NewOrder(cmd, deliveryResp.Price)

    // Сохраняем заказ в хранилище
    if err := s.repo.Save(ctx, order); err != nil {
        return nil, fmt.Errorf("save order failed: %w", err)
    }

    return order, nil
}

Как видите, здесь order-service напрямую зависит от delivery-service через клиент. Чтобы не усиливать связность, полезно:

  • прятать сетевые детали за интерфейсами (DeliveryClient);
  • использовать таймауты и circuit breaker;
  • обрабатывать частичные сбои.

Асинхронное взаимодействие через события

Асинхронная модель уменьшает связность по времени и доступности:

  • services публикуют события (OrderCreated, PaymentCompleted);
  • другие сервисы подписываются и реагируют.

Например:

  • order-service публикует событие OrderCreated;
  • notification-service, analytics-service, fraud-service получают его и выполняют свои действия.

Покажу вам схему обработки в order-service:

// EventBus - абстракция над брокером сообщений
type EventBus interface {
    Publish(ctx context.Context, topic string, event any) error
}

// OrderCreatedEvent - доменное событие для других сервисов
type OrderCreatedEvent struct {
    ID        string    `json:"id"`        // Идентификатор заказа
    UserID    string    `json:"userId"`    // Пользователь
    Total     float64   `json:"total"`     // Сумма заказа
    CreatedAt time.Time `json:"createdAt"` // Время создания
}

// PublishOrderCreated - публикация события после успешного создания заказа
func (s *OrderService) PublishOrderCreated(ctx context.Context, order *Order) error {
    event := OrderCreatedEvent{
        ID:        order.ID,
        UserID:    order.UserID,
        Total:     order.Total,
        CreatedAt: order.CreatedAt,
    }

    // Отправляем событие в брокер сообщений
    return s.eventBus.Publish(ctx, "orders.created", event)
}

Комментарии:

  • eventBus скрывает конкретную реализацию (Kafka, RabbitMQ и т.д.).
  • Сервисы не знают друг о друге напрямую, только о типе события.
  • Такой подход хорошо масштабируется, но усложняет трассировку и отладку.

Смешанная модель

На практике вы почти всегда комбинируете:

  • синхронные вызовы — там, где нужен немедленный результат;
  • события — там, где можно обработать последствия позже.

Например:

  • заказ создается синхронно (клиент ждет ID);
  • уведомления, логирование, аналитика — асинхронно через события.

Структура кода микросервиса в services-segment

Логические слои внутри одного сервиса

Один сервис внутри services-segment часто строят по слоям:

  • transport / handlers — HTTP/gRPC контроллеры;
  • service / use case — бизнес-операции;
  • repository / storage — доступ к БД;
  • clients — HTTP/gRPC клиенты к другим сервисам;
  • events / messaging — работа с брокером.

Давайте посмотрим пример структуры:

/orders-service
  /cmd
    /orders-api          // точка входа API-сервиса
  /internal
    /api                 // HTTP-обработчики
    /service             // бизнес-логика
    /repository          // работа с БД
    /client              // клиенты внешних сервисов
    /events              // публикация и обработка событий
    /config              // конфигурация

Пример: обработчик HTTP в orders-service

Теперь вы увидите, как это выглядит в коде: от HTTP-слоя до доменного сервиса.

// OrderHandler - HTTP-обработчик для работы с заказами
type OrderHandler struct {
    svc *OrderService // Внедренный сервис с бизнес-логикой
}

// CreateOrderHandler - обработчик POST /orders
func (h *OrderHandler) CreateOrderHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    var req CreateOrderRequest
    // Декодируем JSON-запрос
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        // Возвращаем ошибку 400, если запрос некорректный
        http.Error(w, "invalid json", http.StatusBadRequest)
        return
    }

    // Преобразуем запрос в команду бизнес-уровня
    cmd := CreateOrderCommand{
        UserID: req.UserID,
        Items:  mapItems(req.Items),
    }

    // Вызываем бизнес-операцию
    order, err := h.svc.CreateOrder(ctx, cmd)
    if err != nil {
        // Обрабатываем ошибки доменного уровня
        handleCreateOrderError(w, err)
        return
    }

    // Готовим DTO для ответа
    resp := OrderDTO{
        ID:        order.ID,
        Status:    string(order.Status),
        Total:     order.Total,
        CreatedAt: order.CreatedAt,
    }

    // Устанавливаем заголовок и код ответа
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)

    // Отправляем JSON-ответ
    _ = json.NewEncoder(w).Encode(resp)
}

Здесь я размещаю пример, чтобы вам было проще увидеть прохождение запроса:

  • HTTP-слой ничего не знает про БД или другие сервисы;
  • он работает только через OrderService;
  • все бизнес-правила — в слое service.

Надежность и устойчивость services-segment

Таймауты, ретраи и circuit breaker

Когда сервисы внутри services-segment вызывают друг друга, важно защищаться от:

  • долгих ответов;
  • временных сбоев;
  • частично недоступных сервисов.

Основные приемы:

  • ограничивать время запроса таймаутом;
  • повторять запросы (retry) при временных ошибках;
  • отключать проблемный сервис (circuit breaker) на время.

Давайте посмотрим, как можно организовать вызов другого сервиса с таймаутом:

// CallWithTimeout - пример обертки над запросом к другому сервису
func CallWithTimeout(parentCtx context.Context, op func(ctx context.Context) error) error {
    // Создаем контекст с таймаутом 2 секунды
    ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
    defer cancel() // Всегда освобождаем ресурсы контекста

    // Вызываем операцию с ограничением по времени
    if err := op(ctx); err != nil {
        // Если произошла ошибка - возвращаем ее вызывающему коду
        return err
    }

    return nil
}

Комментарии:

  • таймауты лучше конфигурировать, а не хардкодить;
  • разные операции могут требовать разного времени.

Идемпотентность операций

Когда вы используете retry, важно, чтобы операции были идемпотентными:

  • повторный вызов с теми же параметрами не должен приводить к «дублированию» результата.

Например:

  • создание заказа по одному и тому же externalId не должно создавать дубликаты.

Один из способов — использовать идемпотентные ключи и проверки в БД. Давайте разберемся на примере:

-- Уникальный индекс по внешнему идентификатору
CREATE UNIQUE INDEX idx_orders_external_id ON orders (external_id);
// CreateOrder - пример идемпотентного создания заказа
func (r *OrderRepository) CreateOrder(ctx context.Context, o *Order) error {
    // Пытаемся вставить запись в БД
    _, err := r.db.ExecContext(ctx, `
        INSERT INTO orders (id, external_id, user_id, total)
        VALUES ($1, $2, $3, $4)
    `, o.ID, o.ExternalID, o.UserID, o.Total)

    if isUniqueViolation(err) {
        // Если запись уже существует - считаем, что заказ уже создан
        return ErrOrderAlreadyExists
    }

    return err
}

Комментарии:

  • если клиент повторит запрос с тем же externalId, вы получите ту же сущность;
  • это снижает риск дублирующихся операций при сбоях сети.

Версионирование и эволюция services-segment

Зачем версионировать сервисы

Сервисы внутри services-segment живут долго, и их контракты меняются. Без версионирования вы рискуете:

  • ломать клиентов при каждом изменении;
  • усложнить внедрение новых функций;
  • получить зависимость типа «все должны обновиться одновременно».

Хорошая практика:

  • добавлять новые поля в ответ обратно совместимо;
  • делать новые endpoint-ы или новые версии API при ломающих изменениях.

Подходы к версионированию

Чаще всего применяют:

  • версию в URL:
    • /api/v1/orders
    • /api/v2/orders
  • версию в заголовке:
    • X-API-Version: 2

или

  • отдельные gRPC пакеты:
    • orders.v1
    • orders.v2

Если вы меняете:

  • структуру данных;
  • поведение по умолчанию;
  • семантику полей;

то логичнее делать новую версию и поддерживать старую какое-то время.

Совместное существование версий

На практике в services-segment часто живут сразу несколько версий одного сервиса или контракта.

Например:

  • orders-service обрабатывает:
    • /api/v1/orders — старая модель статусов;
    • /api/v2/orders — новая модель с более детальной шкалой.

Внутри доменного слоя вы можете иметь одну расширенную модель и адаптеры к старому и новому формату DTO. Это снижает дублирование логики.

Обеспечение согласованности данных между сервисами

Локальные транзакции против распределенных

Каждый сервис в services-segment владеет своей БД. Это значит:

  • локальные транзакции есть;
  • распределенных транзакций (2PC) лучше избегать.

Вместо распределенных транзакций чаще используют:

  • паттерн Sagas (оркестратор или хореография);
  • eventual consistency — итоговая согласованность.

Пример сценария с заказами:

  • order-service создает заказ в статусе Pending.
  • payment-service пытается списать деньги.
  • если оплата успешна — заказ переводится в статус Paid.
  • если нет — в Canceled.

Между этими шагами может пройти время, но система в итоге придет к согласованному состоянию.

Паттерн Saga

Давайте посмотрим, как можно реализовать простую сагу оркестратором в отдельном сервисе или модуле:

// OrderSaga - оркестратор процесса создания и оплаты заказа
type OrderSaga struct {
    orders  OrderClient   // Клиент order-service
    payment PaymentClient // Клиент payment-service
}

// CreateAndPay - запускает сагу
func (s *OrderSaga) CreateAndPay(ctx context.Context, req CreateAndPayRequest) error {
    // Шаг 1 - создаем заказ
    order, err := s.orders.Create(ctx, req.Order)
    if err != nil {
        // Если заказ не создался - останавливаемся
        return err
    }

    // Шаг 2 - пытаемся списать деньги
    if err := s.payment.Charge(ctx, PaymentRequest{
        OrderID: order.ID,
        Amount:  order.Total,
    }); err != nil {
        // Компенсирующее действие - отмена заказа
        _ = s.orders.Cancel(ctx, order.ID)
        return err
    }

    // Шаг 3 - переводим заказ в статус оплаченного
    return s.orders.MarkPaid(ctx, order.ID)
}

Комментарии:

  • оркестратор знает последовательность шагов;
  • при ошибках он выполняет компенсирующие действия;
  • эта логика может жить либо в отдельном сервисе, либо в доменном сервисе.

Об observability и операционном аспекте services-segment

Логирование

Каждый сервис должен:

  • логировать ключевые бизнес-события;
  • логировать технические ошибки;
  • использовать корреляционные ID для трассировки сквозных запросов.

Пример:

// WithRequestID - мидлварь для установки корреляционного ID
func WithRequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Читаем или генерируем request-id
        reqID := r.Header.Get("X-Request-ID")
        if reqID == "" {
            reqID = uuid.New().String()
        }

        // Кладем ID в контекст
        ctx := context.WithValue(r.Context(), requestIDKey, reqID)

        // Передаем дальше по цепочке
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Комментарии:

  • все сервисы внутри services-segment должны поддерживать единый формат логов;
  • тогда вы сможете искать по одному request-id в распределенной системе.

Метрики и трассировка

Чтобы понимать, что происходит внутри services-segment, важно:

  • собирать метрики (latency, error rate, throughput);
  • использовать распределенную трассировку (OpenTelemetry, Jaeger, Zipkin).

Так вы сможете:

  • найти медленные запросы;
  • увидеть цепочку вызовов между сервисами;
  • быстрее диагностировать проблемы.

Связь services-segment с другими сегментами

Связь с API Gateway / BFF

Чаще всего:

  • клиент (web/mobile) общается с BFF (Backend For Frontend) или API Gateway;
  • gateway маршрутизирует трафик к нужным сервисам из services-segment.

Задача services-segment:

  • предоставить чистые, достаточно мелкие операции;
  • не думать о специфике конкретного фронтенда.

Задача BFF/gateway:

  • агрегировать данные из нескольких сервисов для удобства фронта;
  • заботиться о аутентификации/авторизации на границе.

Связь с data-segment

Сервисы внутри services-segment:

  • напрямую работают с своей БД/кэшем;
  • могут использовать общий брокер сообщений;
  • стараются не шарить одну БД между несколькими сервисами.

Важно правило: каждый сервис владеет своей схемой данных. Если другой сервису нужны данные, он:

  • либо вызывает API владельца данных;
  • либо подписывается на его события и держит локальную проекцию.

Так вы избегаете плотной связности на уровне БД.

Заключение

Services-segment — это центральный слой вашей архитектуры, в котором сосредоточена бизнес-логика. Правильно спроектированный services-segment помогает:

  • разделить ответственность между независимыми сервисами;
  • упростить сопровождение и развитие системы;
  • повысить устойчивость и масштабируемость.

Ключевые мысли, которые стоит закрепить:

  • Сначала думайте о домене и границах контекстов, а уже затем о сервисах.
  • Старайтесь делать сервисы автономными владельцами своих данных.
  • Для взаимодействия комбинируйте синхронные вызовы и асинхронные события.
  • Вводите четкие API-контракты и версионирование.
  • Закладывайте надежность (таймауты, ретраи, идемпотентность, саги).
  • Не забывайте про наблюдаемость: логи, метрики, трассировку.

Если вы будете рассматривать services-segment как четко выделенный архитектурный уровень, а не просто «набор бэкэнд-сервисов», у вас появится понятная структура системы, которую легче масштабировать и развивать.

Частозадаваемые технические вопросы по теме и ответы

Как ограничить «чрезмерный» рост количества сервисов в services-segment

Используйте явные критерии выделения сервисов: отдельный bounded context, отдельная команда, отличные SLA или масштабирование. Периодически проводите ревью архитектуры и объединяйте слишком мелкие сервисы в более крупные, если они сильно связаны и часто меняются вместе.

Как выбрать между REST и gRPC для внутренних сервисов

Для внешних клиентов обычно удобнее REST. Для внутренних вызовов gRPC дает меньшую задержку и строгую типизацию. Хорошая стратегия — публичные интерфейсы строить на REST, а внутренние — на gRPC, используя генерацию клиентов из proto.

Что делать, если нужен общий поиск по данным из разных сервисов

Не тяните данные напрямую из БД разных сервисов. Вместо этого: 1) сервисы публикуют события об изменениях; 2) отдельный search-service подписывается на события и строит индекс (например, в Elasticsearch); 3) клиенты обращаются к search-service, а не к нескольким БД.

Как синхронизировать схемы сообщений между сервисами

Храните схемы событий в едином репозитории схем (schema registry). Используйте формат с явной схемой (Avro, Protobuf). При изменениях добавляйте новые поля опционально, избегая ломающих изменений, либо выпускайте новую версию события с другим типом или топиком.

Как организовать локальную разработку с большим количеством сервисов

Чаще всего: 1) запускают только изменяемый сервис локально; 2) остальные сервисы заменяют mock-ами или тестовыми контейнерами; 3) используют docker-compose для минимального набора зависимостей (БД, брокер, 1–2 критичных сервиса); 4) для сложных сценариев — отдельный «sandbox»-стенд, максимально похожий на прод.

Стрелочка влевоСегмент types - работа со срезами и типами сегментов в GoСегмент hooks - библиотека hooks-segment для интеграции Segment в ReactСтрелочка вправо

Все гайды по Feature-sliced_design

Открыть базу знаний

Отправить комментарий