Опишите ваш опыт в проектировании архитектуры бэкенд-приложений.

Ответ

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

Ключевые принципы, которыми я руководствовался:

  • Разделение по бизнес-доменам (Domain-Driven Design): Каждый сервис отвечал за свою часть бизнес-логики (например, users, orders, payments).
  • Слабая связанность (Loose Coupling): Сервисы должны как можно меньше знать друг о друге, взаимодействуя через публичные контракты (API).
  • Высокая отказоустойчивость: Система должна продолжать работать даже при отказе одного или нескольких компонентов.

Принятые архитектурные решения и их обоснование:

  1. Синхронное взаимодействие: Использовали gRPC для межсервисных вызовов. Выбрали его из-за высокой производительности (бинарный протокол, HTTP/2) и строгой типизации контрактов с помощью .proto файлов.
  2. Асинхронное взаимодействие: Внедрили Apache Kafka для построения Event-Driven архитектуры. Это позволило нам расцепить сервисы: например, сервис заказов публиковал событие OrderCreated, а сервисы уведомлений и склада подписывались на него и реагировали независимо.
  3. Работа с данными: Использовали PostgreSQL с настроенной репликацией (Master-Slave). Запросы на чтение отправлялись на реплики для снижения нагрузки на основную базу данных.
  4. Отказоустойчивость: Применяли паттерны, такие как Circuit Breaker, чтобы избежать каскадных отказов, и компенсирующие транзакции для поддержания консистентности данных в распределенной системе.

Пример кода (сервис заказов):

type OrderService struct {
    repo          OrderRepository // Репозиторий для работы с БД
    eventProducer EventProducer   // Продюсер событий в Kafka
}

func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
    // 1. Создаем и сохраняем заказ в транзакции
    order, err := s.repo.Save(ctx, convertToDomain(req))
    if err != nil {
        return nil, fmt.Errorf("failed to save order: %w", err)
    }

    // 2. Публикуем событие о создании заказа
    event := NewOrderCreatedEvent(order)
    if err := s.eventProducer.Publish(ctx, event); err != nil {
        // Если не удалось отправить событие, откатываем создание заказа
        // Это пример компенсирующей транзакции
        s.repo.Delete(ctx, order.ID)
        return nil, fmt.Errorf("failed to publish event: %w", err)
    }

    return order, nil
}

Ответ 18+ 🔞

Ах ты ж ёпта, ну и тема! Архитектура, говоришь? Да я в этой теме, как рыба в воде, только вместо воды — сплошные микросервисы и асинхронные очереди, блядь.

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

На чём я там мозги ломал, чтобы не сойти с ума:

  • Разделение по смыслу (Domain-Driven Design): Чтобы не было каши, где один сервис и за пользователей отвечает, и за заказы, и за доставку пиццы. Нет, блядь. Каждый — в своей песочнице. users — сидит, логины-пароли считает. orders — заказы строчит. payments — деньги стрижёт. Чётко, ясно, а если один сдох — остальные не сразу обоссываются.
  • Слабая связанность: Это святое! Чтобы сервисы друг про друга знали только то, что им положено по контракту. Не как бабки у подъезда, которые про всех всё знают, а как дипломаты на переговорах — только официальные бумажки.
  • Неубиваемость: Система должна держаться, даже если половина компонентов легла, как после корпоратива. Это главный принцип.

И что мы там понавтыкали, и зачем:

  1. Для быстрого общения между сервисами — gRPC. Выбрали его, потому что он быстрый, как хуй с горы, и строгий. Все контракты прописаны в .proto файлах — никаких «ой, а я думал, ты мне строку пришлёшь, а не число». Чихать хотел, что ты думал, тут всё по протоколу, ёпта.

  2. Для фоновых дел и расцепления — Apache Kafka. Вот это, блядь, вещь! Один сервис сделал дело — кинул событие в очередь («заказ создан!») и пошёл дальше пить кофе. А другие сервисы (склад, уведомления) сами подхватят это событие, когда проснутся. Красота! Никто никого не ждёт, все заняты своим делом. Настоящая event-driven архитектура, а не хухры-мухры.

  3. С данными — PostgreSQL с репликацией. Основная база (мастер) — для записи. А для чтения — целая орава рабов (slave-реплик). Чтоб все эти аналитики и дашборды не ебли основную базу своими тяжёлыми запросами. Пусть рабов мучают.

  4. Чтобы всё не рухнуло разом — паттерны отказоустойчивости. Circuit Breaker — это как предохранитель в розетке. Если один сервис начал тупить и не отвечать, его просто отключают от цепи, чтобы он остальных не подвёл. А компенсирующие транзакции — это на случай, если по цепочке что-то пошло не так. Ну, создали заказ, а событие отправить не смогли. Тогда надо откатить создание заказа, чтобы не было каши в данных. Типа «ой, бля, не вышло — давай по-новой, Муму».

Вот, смотри, как это в коде выглядело (сервис заказов):

type OrderService struct {
    repo          OrderRepository // Это чтобы в базу лазить
    eventProducer EventProducer   // А это — чтобы орать в Кафку, что заказ создан
}

func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
    // 1. Тыкаем заказ в базу
    order, err := s.repo.Save(ctx, convertToDomain(req))
    if err != nil {
        return nil, fmt.Errorf("failed to save order: %w", err)
    }

    // 2. Кричим на всю очередь, что заказ есть!
    event := NewOrderCreatedEvent(order)
    if err := s.eventProducer.Publish(ctx, event); err != nil {
        // Ага, ёпта! Не прокричали. Значит, отменяем всё, что сделали.
        // Чтобы в системе не осталось полузаказа, который никому не нужен.
        s.repo.Delete(ctx, order.ID)
        return nil, fmt.Errorf("failed to publish event: %w", err)
    }

    return order, nil
}

Вот так вот, без пафоса и лишних движений. Сделал дело — зафиксировал. Не смог зафиксировать — откатил, как будто ничего и не было. Чистая работа, без подлянок. А то бывает, накосячил, а потом полгода ищешь, где ж эта хитрая жопа данных закопала неконсистентность.