Приведите пример реализации сложной бизнес-логики.

Ответ

Сложная бизнес-логика часто связана с обеспечением согласованности в распределенных системах и динамическими правилами.

1. Паттерн Saga для распределенных транзакций в микросервисах: Вместо единой ACID-транзакции используется последовательность локальных транзакций, каждая из которых публикует событие для запуска следующего шага. При ошибке выполняются компенсирующие транзакции (обратные действия).

2. Система динамических правил валидации: Правила хранятся в базе данных или конфигурации и могут изменяться без перезапуска приложения.

Пример класса валидатора заказа:

public class OrderValidator {
    private RuleEngine ruleEngine;

    public ValidationResult validate(Order order) {
        // Загрузка актуальных правил из внешнего источника
        List<BusinessRule> rules = ruleEngine.fetchActiveRules("ORDER_VALIDATION");

        ValidationResult result = new ValidationResult();
        for (BusinessRule rule : rules) {
            if (!rule.evaluate(order)) {
                result.addError(rule.getCode(), rule.getFailureMessage());
            }
        }
        return result;
    }
}
// Пример правила (псевдокод)
public class StockAvailabilityRule implements BusinessRule {
    @Override
    public boolean evaluate(Order order) {
        return order.getItems().stream()
                .allMatch(item -> inventoryService.isAvailable(item.getId(), item.getQuantity()));
    }
}

3. Обработка событий в Event-Driven архитектуре: Использование брокера (Kafka, RabbitMQ) для асинхронной обработки. Важна идемпотентность обработчиков и механизмы повторных попыток (retry) и dead letter queues.

Ответ 18+ 🔞

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

Вот смотри, классика жанра — распределённые транзакции. Раньше было просто: одна большая транзакция на всю БД, накрыл всех COMMIT'ом — и спать. А теперь? Теперь у тебя микросервисы, каждый со своей базой, как ёбаный одинокий волк. Как между ними согласованность обеспечивать? ACID на всю систему — это уже не катит, это как пытаться управлять танцем лебедей, сидя на унитазе.

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

Но, а если на третьем шаге всё пошло по пизде? Клиентская карта declined? Тут-то и включается мозг. Для каждого шага надо заранее придумать компенсирующую транзакцию — обратное действие, которое откатит изменения этого конкретного шага. Не удалось списать деньги после резерва товара? Значит, запускаем компенсацию, которая разрезервирует этот товар обратно. Получается такая волна: вперёд покатилась, споткнулась — и откатилась назад, отменяя всё, что успела наворотить. Главное, чтобы эти компенсации были идемпотентными, а то наотменяешь товара в минус, овердохуища.

А теперь второй слой маразма — динамические правила. Бизнес говорит: «Хочу, чтобы заказ больше 100 тысяч рублей автоматически отправлялся на ручную проверку». А через неделю: «А теперь пусть проверяется, если клиент из нового региона». А ещё через день: «Всё, отмена, передумали, но про регион оставьте». Если для каждого такого чиха перезапускать сервис и катить релиз — это ж пидары налетят, DevOps тебя живьём сожрёт.

Поэтому правила выносим наружу. В базу, в конфиг-сервер, куда угодно. Берём валидатор, и он перед проверкой тупо дёргает актуальный список правил. Смотри, какой код простой, аж глаз радуется:

public class OrderValidator {
    private RuleEngine ruleEngine;

    public ValidationResult validate(Order order) {
        // Загрузка актуальных правил из внешнего источника
        List<BusinessRule> rules = ruleEngine.fetchActiveRules("ORDER_VALIDATION");

        ValidationResult result = new ValidationResult();
        for (BusinessRule rule : rules) {
            if (!rule.evaluate(order)) {
                result.addError(rule.getCode(), rule.getFailureMessage());
            }
        }
        return result;
    }
}

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

public class StockAvailabilityRule implements BusinessRule {
    @Override
    public boolean evaluate(Order order) {
        return order.getItems().stream()
                .allMatch(item -> inventoryService.isAvailable(item.getId(), item.getQuantity()));
    }
}

Изменили правило в базе — и оно тут же, без всяких деплоев, начало работать. Волшебство, блядь! Правда, есть нюанс: если само правило будет долго выполняться или падать, то и валидация вся встанет. Но это уже детали, на то ты и инженер, чтобы продумать.

Ну и куда же без Event-Driven архитектуры? Все друг другу события шлют, как мартышлюшки в зоопарке бананами. Отправил событие «Заказ создан» в Кафку — и пошла писать губерния: сервис инвентаря резервирует товар, сервис платежей дерёт деньги, сервис доставки грузит в машину.

Но тут, чувак, главное — идемпотентность. Потому что брокер может доставить тебе одно и то же сообщение три раза, а обработать его надо ровно один. Иначе товар задваивается, деньги списываются дважды — короче, пиздец. Ставь уникальные ID событий, проверяй, не обрабатывал ли уже такое.

И обязательно настрой retry для временных ошибок и Dead Letter Queue для совсем безнадёжных случаев. Чтобы если сервис упал, сообщение не потерялось, а спокойно ждало в углу, пока его снова не попробуют обработать. А если уж совсем хана — упало в DLQ, и туда уже смотрит оповещённый разработчик с лицом, как после трёх дней в седле.

Вот так и живём: саги, правила, события. Сложно? Ебать, да. Но когда это всё работает как часы — чувствуешь себя богом распределённых систем. Пока, конечно, в три часа ночи не сломается.