Был ли случай когда отстаивал инициативу и оказался не прав
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Случай когда я отстаивал инициативу и оказался не прав
Да, было несколько таких случаев. Рассказываю о одном из самых поучительных.
Контекст
Я работал в fintech компании, где мы мигрировали legacy Java 8 приложение на Java 17 с переходом на новый фреймворк. Я был одним из архитекторов этого проекта.
Моя инициатива
На этапе дизайна API я настоял на использовании полностью асинхронного подхода с Project Reactor (Spring WebFlux) вместо традиционного Spring MVC:
// Мой предложенный подход
@RestController
@RequestMapping("/api/transactions")
public class TransactionController {
@GetMapping("/{id}")
public Mono<TransactionResponse> getTransaction(@PathVariable String id) {
return transactionService.getTransactionAsync(id)
.map(TransactionMapper::toResponse);
}
@PostMapping
public Mono<TransactionResponse> createTransaction(
@RequestBody Mono<TransactionRequest> request) {
return request
.flatMap(transactionService::createTransactionAsync)
.map(TransactionMapper::toResponse);
}
}
Почему я это отстаивал
Мои аргументы были:
- Масштабируемость — асинхронность позволит обрабатывать больше запросов с меньше потоков
- Современность — Java 17 стоит использовать с современными паттернами
- Производительность — меньше context switches, меньше потребление памяти
- Будущеность — Virtual Threads (Project Loom) будут лучше работать с асинхронным кодом
Я провел presentation, показал бенчмарки нагрузочного тестирования, убедил архитектуру комитет. Команда согласилась.
Что пошло не так
После 3 месяцев разработки выяснилось несколько проблем:
1. Сложность отладки
Отладка асинхронного кода в боевых условиях оказалась намного сложнее. Stack trace'ы невозможно читать:
java.lang.RuntimeException: Something went wrong
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onNext
at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext
at reactor.core.publisher.Flux$5.subscribe
at reactor.core.publisher.FluxDefer.subscribe
[... 50 more lines]
Отследить откуда именно пришла ошибка — nightmare для junior разработчиков.
2. Усложнение бизнес-логики
Просто логика вроде "сначала получить транзакцию, потом проверить баланс, потом обновить" превращалась в:
// Вместо простого
Transaction tx = transactionService.getTransaction(id);
if (!accountService.hasBalance(tx.getAccountId(), tx.getAmount())) {
throw new InsufficientFundsException();
}
transactionService.updateStatus(tx.getId(), "CONFIRMED");
// Писали вот это
return transactionService.getTransactionAsync(id)
.filterWhen(tx -> accountService.hasBalanceAsync(
tx.getAccountId(), tx.getAmount())
.map(has -> {
if (!has) {
throw new InsufficientFundsException();
}
return has;
})
)
.flatMap(tx -> transactionService.updateStatusAsync(tx.getId(), "CONFIRMED")
.map(ignore -> tx)
);
Многие разработчики выбирали неправильные операторы Reactor (flatMap vs map vs concatMap), что приводило к deadlock-подобным ситуациям.
3. Нехватка опыта в команде
Оказалось, что реактивное программирование — это не просто async/await. Это другой парадигма:
- Моноиды, композиция, функциональный подход
- Backpressure
- Scheduler'ы
- Marble diagrams
Треть команды получила проблемы с production:
reactor.core.Exceptions$ErrorCallbackNotImplementedException:
Received fatal signal:
java.lang.OutOfMemoryError
Оказалось, junior developer забыл добавить .onErrorResume() и все ошибки буферизировались в памяти.
4. Интеграция с legacy системами
Оказалось, что несколько critical сервисов (платежные шлюзы, compliance system) были синхронными и не поддерживали асинхронный вызовы. Пришлось делать оберки с .block(), что убило весь смысл асинхронности:
// Убийца асинхронности
return transactionService.getTransactionAsync(id)
.map(tx -> {
// block() = death of async
var paymentResult = legacyPaymentService.process(tx).block();
return tx;
});
Что произошло дальше
Менеджер проекта провел meeting. Я признал свою ошибку и предложил две опции:
Опция A: Откатиться
Перейти назад на Spring MVC, потратить 2 недели на рефакторинг.
Опция B: Гибридный подход
Комбинировать асинхронность только где она имеет смысл (внешние HTTP API), остальное оставить синхронным.
Мы выбрали опцию B. Это было правильное решение.
Что я выучил
-
Не влюбляйся в технологию — выбирай инструмент для конкретной проблемы, не наоборот
-
Уважай уровень команды — асинхронный код требует старшего уровня разработчиков
-
Проверяй assumptions — я не проверил реальную пропускную способность legacy систем
-
Инвести в education — нужно было провести training до начала разработки
-
Выслушивай feedback — когда видишь первые проблемы, это сигнал к переоценке
Вывод
Этот случай научил меня быть скромнее в отношении своих идей. Теперь я:
- Начинаю с MVP (минимальный жизнеспособный продукт)
- Слушаю feedback дольше
- Не отстаиваю идею, если команда против
- Предоставляю escape routes в своих дизайнах
Это был дорогой урок, но очень ценный для моего профессионального развития. Хороший архитектор не просто делает правильный выбор первый раз — он оставляет возможность эволюции и адаптации.