Building a Payment Approval Integration Event Handler

Building a Payment Approval Integration Event Handler

1. Introduction

In a microservices architecture (MSA) environment, each domain service has independent business responsibilities and a database. The attendance and leave domain (Timecard/Leave services) in this system also operates through its own lifecycle; however, core business processes such as users' leave applications (LeavePlan) and special work applications require close integration with the enterprise-wide common domain, the External Approval Service. The existing process handled approval statuses internally within the service, but the standardization of company-wide electronic approvals required the construction of an integration pipeline with the external approval service. However, if communication with an external approval system is handled through a simple synchronous approach in an MSA environment, delays or failures in the approval server can propagate to internal services as transaction blocking. In addition, because approval-line approval (Approved), rejection (Rejected), and withdrawal (Withdrawn) occur asynchronously after a user submits a draft, an architecture that guarantees eventual consistency between the two services was required. This article shares a practical implementation case in which an ApprovalStatusHandler was built in the inbound layer of timecard-facade and combined with SubmitLeaveRequestTask and LeavePlanTask to complete an asynchronous, event-driven approval status synchronization pipeline in an MSA environment.

2. Background for the Technology Selection

The key challenges that had to be addressed when designing the integration architecture with the external approval service were as follows.

  1. Fault isolation and asynchronous decoupling: We adopted a structure that receives the result of approval completion as an asynchronous event message so that traffic spikes or maintenance of the external approval system would not compromise the availability of the attendance service.

  2. Domain mapping based on the external identifier (approvalId): The approval document identifier (approvalId) generated by the external approval service was bound to the internal leave application entity (LeavePlan) in the form of a foreign key (Reference Key), enabling accurate lookup of the target domain when asynchronous status-change events are received later.

  3. Event idempotency and state-transition validation: We designed an entity-level idempotency guard (State-based Guard) to prevent internal data corruption even if the same approval-completion event was received multiple times due to network retransmission.

Therefore, we introduced a state synchronization pattern based on an inbound event handler as a solution that minimizes coupling between systems while safely tracking state changes in the external service and ensuring data integrity in a distributed environment.

3. Implementation Process and Troubleshooting

Based on the design principles of the actual approval integration pipeline in this project, this section explains in detail how the external approval service was safely integrated with the internal domain, including specific code blocks.

3.1 Approval Service Submission Request and Identifier Mapping (SubmitLeaveRequestTask)

When a user submits a leave application, SubmitLeaveRequestTask validates whether the external approval document identifier (approvalId) has been provided and sets the initial status to InApproval (approval in progress). After performing validation (checking the remaining days and overlapping periods), it persists approvalId in the LeavePlan entity, thereby completing a unidirectional mapping structure that can track approval result events arriving asynchronously later.

@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 Receiving and Routing Approval Status Events (ApprovalStatusHandler)

When a status change occurs in the external electronic approval system, such as final approval on the approval line, rejection, or withdrawal of the submitted draft, the event payload enters through the inbound consumer channel. ApprovalStatusHandler serves as an adapter that isolates the external approval service's event specification so that it does not directly penetrate the internal domain core, and routes processing to the appropriate domain Task method according to the received approval status (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 Domain Idempotency Validation and Status Update (LeavePlanTask)

LeavePlanTask queries the internal domain entity based on the external approval identifier (approvalId). It performs an idempotency guard (State-based Guard) by checking the entity's current status (PlanStatus.Approved) in advance, ensuring that the system does not malfunction even if an approval event that has already been completed is received again due to network retries or duplicate message delivery. Only valid transitions result in a status change and the persistence of approval metadata, after which a final approval-completed domain event is published to safely trigger subsequent processes.

@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 Troubleshooting: Resolving Mismatches Between Approval Status Changes and DB Check Constraints

In the early stages of integrating the approval service, a DB-level CHECK (plan_status IN (...)) constraint violation occurred while the internal domain status was transitioning after the user submitted an approval request. The existing internal state-machine schema defined only status values based on the service's own approval process, so the detailed lifecycle status (InApproval) added for integration with the external approval service was missing. To resolve this issue, we applied a Flyway migration script to synchronize the database constraints with the latest approval lifecycle and added an exception-state rollback guard to LeavePlanTask to ensure runtime data consistency.

4. Results and Achievements

Applying the asynchronous approval integration event-handler architecture to the company's major attendance and leave domains produced the following technical achievements.

  • Reduced coupling between MSA systems and fault isolation: We achieved complete fault isolation, ensuring that traffic spikes or temporary failures in the external electronic approval service did not propagate to the core functions of the attendance service.

  • Ensuring distributed data consistency: Through an idempotent handler design based on the external approval identifier (approvalId), we reliably maintained consistency between approval documents and internal domain data without duplicate event processing or missed processing caused by network retries.

  • Establishment of a standard integration architecture: By layering and modularizing ApprovalStatusHandler, we established an internal standard integration pattern that can be consistently reused when expanding into new domains requiring approval service integration, such as special work and business travel.

5. Conclusion

Implementing this approval integration event handler provided an opportunity to appreciate the importance of backend architecture design that reduces coupling between domains and reliably protects data integrity when communicating with external services in an MSA environment.

By avoiding simple synchronous API calls and introducing an asynchronous event handler (ApprovalStatusHandler) along with entity-level idempotent state-update design, we were able to achieve both fault isolation between systems and eventual consistency. We intend to turn the external service integration patterns and state synchronization expertise established through this project into internal standard guidelines, thereby contributing to the next level of architectural stability for the company's distributed systems.

* Reference

  • Enterprise Integration Patterns - Event-Driven Consumer & Message Dispatcher

  • Chris Richardson - Microservices Patterns (Saga and Asynchronous Messaging)

  • Spring Framework Official Documentation - Integration and Event Listeners

D.Hyeok

Site footer