Представьте: ваше приложение на Go похоже на перегруженного работой официанта в ресторане, отмеченном звездой Мишлен. Оно принимает заказы (записи), подаёт блюда (чтения), подливает напитки (обновления) и разбирается с «хочу поговорить с менеджером» Кэрри — всё это в неудобных туфлях. На помощь приходит CQRS — архитектурный аналог найма команды шеф-поваров и сомелье. Давайте создадим масштабируемое решение!

Почему вашему коду нужна терапия (и CQRS)

Традиционный CRUD подобен использованию швейцарского армейского ножа для проведения нейрохирургии — возможно, но грязно. CQRS (Command Query Responsibility Segregation) разделяет ваше приложение на:

  1. Сторона команд: «пишущие» повара, управляющие изменениями состояния.
  2. Сторона запросов: «читающий» обслуживающий персонал, подающий данные в качестве закуски.
graph LR Client-->|Command|CommandHandler CommandHandler-->|Event|EventStore[(Kafka)] EventStore-->|Publish|QueryService QueryService-->ReadDB[(Read DB)] Client-->|Query|QueryService

Это разделение позволяет оптимизировать каждую сторону независимо — как если бы у вас были спортивные автомобили с турбонаддувом для записи и роскошные лимузины для чтения.

Реализация CQRS в Go: пошаговый рецепт

Шаг 1: Командуйте на кухне

Создадим структуры команд. Думайте о них как о рецептурных карточках для изменения состояния:

type CreateOrderCommand struct {
    OrderID     uuid.UUID
    UserID      uuid.UUID
    Items       []Item
    SpecialNote string `json:"special_note" validate:"omitempty,max=500"`
}
type CancelOrderCommand struct {
    OrderID    uuid.UUID
    Reason     string `json:"reason" validate:"required"`
    CancelledBy uuid.UUID
}

Наш обработчик команд действует как шеф-повар:

type OrderCommandHandler struct {
    eventProducer EventProducer
}
func (h *OrderCommandHandler) Handle(ctx context.Context, cmd interface{}) error {
    switch command := cmd.(type) {
    case CreateOrderCommand:
        if err := validate.Struct(command); err != nil {
            return fmt.Errorf("invalid order: %w", err)
        }
        event := OrderCreatedEvent{
            OrderID:     command.OrderID,
            Timestamp:   time.Now().UTC(),
            MenuItems:   command.Items,
            SpecialNote: command.SpecialNote,
        }
        return h.eventProducer.Publish(ctx, event)
    // Обработка других команд...
    }
    return ErrUnknownCommand
}

Шаг 2: Kafka как ваш помощник по работе с событиями

Настроим наш Kafka-продюсер для трансляции событий заказа:

type KafkaEventProducer struct {
    producer sarama.SyncProducer
    topic    string
}
func (k *KafkaEventProducer) Publish(ctx context.Context, event Event) error {
    jsonData, _ := json.Marshal(event)
    msg := &sarama.ProducerMessage{
        Topic: k.topic,
        Value: sarama.ByteEncoder(jsonData),
    }
    _, _, err := k.producer.SendMessage(msg)
    if err != nil {
        return fmt.Errorf("failed to send Kafka message: %w", err)
    }
    return nil
}

Совет: добавьте идентификаторы корреляции к вашим событиям — они похожи на номера отслеживания рецептов для ваших распределённых транзакций.

Шаг 3: Модели запросов — стол для гастрономических критиков

Наша модель чтения использует материализованные представления, оптимизированные для конкретных запросов:

type OrderProjection struct {
    db *sqlx.DB
}
func (p *OrderProjection) HandleOrderCreated(event OrderCreatedEvent) error {
    return p.db.Exec(`
        INSERT INTO active_orders 
        (id, user_id, items, special_notes, status)
        VALUES ($1, $2, $3, $4, 'PENDING')`,
        event.OrderID,
        event.UserID,
        event.Items,
        event.SpecialNote,
    )
}
func (p *OrderProjection) GetActiveOrders(userID uuid.UUID) ([]OrderSummary, error) {
    var orders []OrderSummary
    err := p.db.Select(&orders, `
        SELECT id, items, status 
        FROM active_orders 
        WHERE user_id = $1`, userID)
    return orders, err
}

Распространённые проблемы на кухне (и как их устранить)

  1. Драма с согласованностью по итогу:
    • Используйте проверки версий в командах.
    • Реализуйте уведомления для пользователей о «выполнении операции».
    • Добавьте компенсирующие действия для критических путей.
  2. Кошмар с штормом событий:
    // Плохое событие — слишком расплывчато
    type OrderUpdatedEvent struct{} 
    // Хорошее событие — конкретное изменение состояния
    type OrderLocationUpdatedEvent struct {
        NewLat  float64
        NewLong float64
        UpdatedBy uuid.UUID
    }
    
  3. Паника из-за отставания Kafka-потребителя:
    • Эффективно используйте группы потребителей.
    • Мониторьте отставание с помощью метрик Prometheus.
    • Реализуйте очереди мёртвых писем для проблемных сообщений.

Секретный соус: когда добавлять эту приправу

CQRS эффективен, когда:

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

Напоследок: мудрость (и шутки отца)

Реализация CQRS похожа на обучение вашего кода танго — требуется практика, но когда всё синхронизируется, это красиво. Помните:

  • «Исторический источник событий» — это не просто паттерн, это образ жизни.
  • Потоки Kafka — это реки истины в вашей системе.
  • Хороший идентификатор корреляции стоит своего веса в журналах отладки. Как мы говорим в распределённых системах: «Если с первого раза у вас не получилось, уничтожьте все доказательства своих попыток». Подождите, нет — это не так. Лучше придерживаться «согласованности по итогу»!

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