1. Введение
В среде микросервисной архитектуры (MSA) каждый доменный сервис имеет независимые бизнес-обязанности и собственную базу данных. Домен учета рабочего времени и отпусков (сервисы Timecard/Leave) в этой системе также функционирует в рамках собственного жизненного цикла. Однако ключевые бизнес-процессы, такие как подача пользователями заявок на отпуск (LeavePlan) и заявок на особый режим работы, требуют тесной интеграции с общекорпоративным доменом — внешним сервисом согласований. Изначально статусы согласования обрабатывались внутри сервиса, но стандартизация электронных согласований на уровне компании потребовала построения интеграционного конвейера с внешним сервисом согласований. Однако если взаимодействие с внешней системой согласований в среде MSA реализуется простым синхронным способом, задержки или сбои сервера согласований могут распространяться на внутренние сервисы и блокировать транзакции. Кроме того, поскольку согласование по цепочке (Approved), отклонение (Rejected) и отзыв (Withdrawn) происходят асинхронно после отправки пользователем черновика, потребовалась архитектура, гарантирующая согласованность данных между двумя сервисами в конечном итоге. В статье рассматривается практический вариант реализации, в котором во входящем слое timecard-facade был создан ApprovalStatusHandler, объединенный с SubmitLeaveRequestTask и LeavePlanTask для построения асинхронного конвейера синхронизации статусов согласования на основе событий в среде MSA.
2. Обоснование выбора технологии
При проектировании интеграционной архитектуры с внешним сервисом согласований необходимо было решить следующие ключевые задачи.
-
Изоляция сбоев и асинхронное разделение: мы выбрали структуру, принимающую результат завершения согласования в виде асинхронного сообщения-события, чтобы скачки нагрузки или техническое обслуживание внешней системы согласований не снижали доступность сервиса учета рабочего времени.
-
Сопоставление доменов на основе внешнего идентификатора (approvalId): идентификатор документа согласования (approvalId), создаваемый внешним сервисом согласований, связывался с внутренней сущностью заявки на отпуск (LeavePlan) в виде внешнего ключа (Reference Key). Это обеспечивало точный поиск целевого домена при последующем получении асинхронных событий изменения статуса.
-
Идемпотентность событий и проверка переходов состояний: мы разработали защиту от повторной обработки на уровне сущности (State-based Guard), предотвращающую повреждение внутренних данных даже в случае многократного получения одного и того же события завершения согласования из-за повторной передачи по сети.
Поэтому в качестве решения мы внедрили шаблон синхронизации состояний на основе обработчика входящих событий. Он минимизирует связанность между системами, безопасно отслеживает изменения состояния во внешнем сервисе и обеспечивает целостность данных в распределенной среде.
3. Процесс реализации и устранение неполадок
Опираясь на принципы проектирования фактического конвейера интеграции с сервисом согласований в рамках этого проекта, в данном разделе подробно объясняется, как внешний сервис согласований был безопасно интегрирован с внутренним доменом, включая конкретные фрагменты кода.
3.1. Запрос на отправку в сервис согласований и сопоставление идентификатора (SubmitLeaveRequestTask)
Когда пользователь отправляет заявку на отпуск, SubmitLeaveRequestTask проверяет, указан ли внешний идентификатор документа согласования (approvalId), и устанавливает исходный статус InApproval (согласование выполняется). После выполнения проверок (остатка доступных дней и пересечений периодов) approvalId сохраняется в сущности LeavePlan, что завершает создание однонаправленной структуры сопоставления, позволяющей отслеживать события с результатами согласования, поступающие позднее асинхронно.
@Service
@RequiredArgsConstructor
public class SubmitLeaveRequestTask {
private final LeavePlanLogic leavePlanLogic;
private final LeavePermitLogic leavePermitLogic;
public String submitLeaveRequest(String citizenId, LocalDate startDate, LocalDate endDate,
LeaveType leaveType, double leaveDays, String approvalId) {
LeavePermit leavePermit = getLeavePermit(citizenId, startDate.getYear());
leavePermit.planLeave(leaveDays);
leavePermitLogic.modifyLeavePermit(leavePermit);
LeavePlanCdo leavePlanCdo = LeavePlanCdo.builder()
.citizenId(citizenId)
.leaveType(leaveType)
.planStatus(StringUtils.hasText(approvalId) ? PlanStatus.InApproval : PlanStatus.Planned)
.approvalId(approvalId)
.build();
return leavePlanLogic.registerLeavePlan(leavePlanCdo);
}
}
3.2. Получение событий изменения статуса согласования и их маршрутизация (ApprovalStatusHandler)
Когда во внешней системе электронных согласований происходит изменение статуса — например, окончательное согласование по цепочке, отклонение или отзыв отправленного черновика, — данные события поступают через входящий канал потребителя. ApprovalStatusHandler выступает адаптером, изолирующим спецификацию событий внешнего сервиса согласований, чтобы она не проникала напрямую во внутреннее ядро домена, и направляет обработку в соответствующий метод доменной Task в зависимости от полученного статуса согласования (status).
@Component
@RequiredArgsConstructor
public class ApprovalStatusHandler {
private final LeavePlanTask leavePlanTask;
public void handleApprovalStatusChanged(ApprovalStatusEventPayload payload) {
String approvalId = payload.getDocumentId();
ApprovalStatus status = payload.getStatus();
switch (status) {
case Approved -> leavePlanTask.approveLeavePlanByApprovalId(approvalId, payload.getApproverId());
case Rejected -> leavePlanTask.rejectLeavePlanByApprovalId(approvalId, payload.getRejectReason());
case Cancel -> leavePlanTask.cancelLeavePlanByApprovalId(approvalId);
default -> {
log.info("Approval transient state received: {}", status);
}
}
}
}
3.3. Проверка идемпотентности домена и обновление статуса (LeavePlanTask)
LeavePlanTask выполняет поиск сущности внутреннего домена по внешнему идентификатору согласования (approvalId). Затем он применяет защиту от повторной обработки (State-based Guard), предварительно проверяя текущий статус сущности (PlanStatus.Approved). Благодаря этому система не дает сбоя даже в случае повторного получения уже обработанного события согласования из-за повторных сетевых запросов или доставки дублирующего сообщения. Только допустимые переходы приводят к изменению статуса и сохранению метаданных согласования, после чего публикуется финальное доменное событие о завершении согласования, безопасно запускающее последующие процессы.
@Component
@RequiredArgsConstructor
public class LeavePlanTask {
private final LeavePlanStore leavePlanStore;
private final EventPublisher eventPublisher;
@Transactional
public void approveLeavePlanByApprovalId(String approvalId, String approverId) {
LeavePlan leavePlan = leavePlanStore.findByApprovalId(approvalId)
.orElseThrow(() -> new NoSuchElementException("연동된 휴가 건이 없습니다: " + approvalId));
if (leavePlan.getPlanStatus() == PlanStatus.Approved) {
return;
}
leavePlan.setPlanStatus(PlanStatus.Approved);
leavePlan.setApproverId(approverId);
leavePlan.setApprovedTime(System.currentTimeMillis());
leavePlanStore.update(leavePlan);
eventPublisher.publish(new LeavePlanEvent(leavePlan.getId(), leavePlan.getRequesterId(), PlanStatus.Approved));
}
}
3.4. Устранение неполадок: устранение несоответствий между изменениями статуса согласования и ограничениями проверки БД
На ранних этапах интеграции с сервисом согласований произошло нарушение ограничения CHECK на уровне БД (plan_status IN (...)) при переходе статуса внутреннего домена после отправки пользователем запроса на согласование. В существующей схеме внутреннего автомата состояний были определены только значения статуса, основанные на собственном процессе согласования сервиса, поэтому подробный статус жизненного цикла (InApproval), добавленный для интеграции с внешним сервисом согласований, отсутствовал. Для решения проблемы мы применили скрипт миграции Flyway, синхронизировав ограничения базы данных с актуальным жизненным циклом согласования, а также добавили в LeavePlanTask защиту отката при состоянии исключения, чтобы обеспечить согласованность данных во время выполнения.
4. Результаты и достижения
Применение архитектуры обработчика событий асинхронной интеграции согласований к основным доменам учета рабочего времени и отпусков компании позволило достичь следующих технических результатов.
-
Снижение связанности между системами MSA и изоляция сбоев: мы обеспечили полную изоляцию сбоев, благодаря чему скачки нагрузки или временные сбои во внешнем сервисе электронных согласований не распространялись на основные функции сервиса учета рабочего времени.
-
Обеспечение согласованности распределенных данных: благодаря идемпотентной конструкции обработчика на основе внешнего идентификатора согласования (approvalId) мы надежно поддерживали согласованность между документами согласования и данными внутреннего домена, не допуская повторной или пропущенной обработки из-за повторных сетевых запросов.
-
Формирование стандартной интеграционной архитектуры: благодаря послойной организации и модульности ApprovalStatusHandler мы создали внутренний стандартный интеграционный шаблон, который можно единообразно использовать при расширении на новые домены, требующие интеграции с сервисом согласований, например на процессы особого режима работы и служебных командировок.
5. Заключение
Реализация этого обработчика событий интеграции с сервисом согласований позволила оценить важность проектирования серверной архитектуры, которая снижает связанность между доменами и надежно защищает целостность данных при взаимодействии с внешними сервисами в среде MSA.
Отказавшись от простых синхронных вызовов API и внедрив асинхронный обработчик событий (ApprovalStatusHandler) вместе с идемпотентным проектированием обновления состояния на уровне сущности, мы смогли одновременно обеспечить изоляцию сбоев между системами и согласованность данных в конечном итоге. Мы планируем преобразовать шаблоны интеграции с внешними сервисами и накопленную в рамках проекта экспертизу по синхронизации состояний во внутренние стандартные рекомендации, тем самым способствуя дальнейшему повышению архитектурной устойчивости распределенных систем компании.
* Ссылки
-
Enterprise Integration Patterns — обработчик, управляемый событиями, и диспетчер сообщений
-
Chris Richardson — Microservices Patterns (Saga и асинхронный обмен сообщениями)
-
Официальная документация Spring Framework — интеграция и обработчики событий
D.Hyeok