Design and Implementation of a Payment Center System: Flow, Challenges, and Design Patterns

This article explains the architecture of a payment center that unifies internal payment services and external third‑party integrations, details the three‑step payment flow, discusses common issues such as order timeout and result reliability, and demonstrates how template‑method and strategy patterns together with RocketMQ delay queues solve these problems in a SpringBoot backend.

Architect's Guide
Architect's Guide
Architect's Guide
Design and Implementation of a Payment Center System: Flow, Challenges, and Design Patterns

The payment center system provides unified payment, refund, and related services for internal business lines while integrating third‑party payment platforms or banks to enable fund flow.

Most companies adopt a similar architecture because it offers several advantages:

Creates a unified payment service, reducing integration and duplicate development costs.

Accelerates support for innovative business, fostering rapid growth.

Facilitates building a secure, stable, and scalable payment system.

Enables centralized accumulation and unified utilization of core payment data.

Payment Process

The diagram shows the main user payment flow, which consists of three steps:

User triggers the cashier page from the order confirmation page.

User selects a payment method on the cashier page, confirms payment, is redirected to the third‑party payment page, enters the password, and completes the payment.

The system processes the payment result and notifies the user and all related subsystems.

1. Invoking the Merchant Cashier

User clicks the "Go to Pay" button on the order confirmation page, which calls the cashier‑order‑creation API.

The cashier caches the order information, stores it, and appends the order identifier to the cashier URL returned to the order system.

The order system receives the cashier URL and redirects the user to the cashier page.

The cashier page is typically divided into three areas: remaining time and payable amount, order information (provided by the business line), and configurable payment channels.

2. User Confirms Payment

User selects a payment channel (Alipay, WeChat Pay, bank card, etc.) and clicks the "Pay Now" button.

The payment center creates an order, calls the third‑party order‑creation API, receives the payment parameters, and returns them to the cashier.

The cashier forwards these parameters to the third‑party gateway, which opens the third‑party payment page.

User enters the password and completes the payment.

3. Payment Result Handling

The third‑party system performs the deduction and returns a result to the cashier (e.g., WeChat returns "PAYING", Alipay returns a final state).

The following steps are executed asynchronously and may occur in any order.

The cashier receives the third‑party result; if payment is confirmed, it schedules a timed task to poll the final status and redirects the user accordingly.

The third‑party notifies the payment center of the final result; the payment center processes the logic and asynchronously notifies the order system.

If no notification is received within a timeout, the payment center actively queries the third‑party to obtain the final status.

The order system may also poll the payment center to avoid missing notifications.

Common Issues and Solutions in the Payment Center

1. Order Timeout and Automatic Closure

When a user does not complete payment within a predefined timeout, the order should be automatically closed. Common implementations include:

Polling the Database

Periodically query the DB for orders that have exceeded the timeout and close them. Drawbacks: latency depends on the polling interval and it adds load to the database.

JDK DelayQueue or Time‑Wheel Algorithms

These in‑memory approaches suffer from data loss on service restart and memory constraints.

RocketMQ Delay Queue

RocketMQ supports delayed messages (18 predefined levels). For a 30‑minute timeout, send a delayed message at level 16; the consumer processes the closure after the delay.

1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

We currently use RocketMQ delay queues for order closure.

2. Ensuring Real‑Time Payment Result

Although third‑party systems push callbacks in 99.9% of cases, network issues or service outages can cause delays. To guarantee timeliness, we also schedule active query tasks using RocketMQ delay queues.

After successful order creation, send a payment‑query delayed message.

The consumer checks whether the payment has reached a final state; if not, it calls the third‑party query API and retries according to configured rules.

If the maximum retry count is reached without success, the task stops.

Key configuration parameters (can be stored in a config center):

// initial delay level, e.g., 3 corresponds to 10s
private Integer queryInitLevel = 3;

// retry count
private Integer queryCount = 6;

// delay levels for retries: 5s,10s,30s,1m,10m,20m
private String queryDelayLevels = 2,3,4,5,14,15;

Sending the delayed query task:

public void payQueryTask(String orderNo) {
    PayQueryMessage payQueryMessage = new PayQueryMessage();
    payQueryMessage.setOrderNo(orderNo);

    RetryMessage<PayQueryMessage> retryMessage = new RetryMessage<>();
    retryMessage.setTotalCount(queryCount);
    retryMessage.setDelayLevels(queryDelayLevels);
    retryMessage.setTopic(TopicConst.PAY_QUERY_TOPIC);
    retryMessage.setEventType(RetryEventTypeEnum.PAY_QUERY);
    retryMessage.setEventDesc(RetryEventTypeEnum.PAY_QUERY.getDesc());
    retryMessage.setData(payQueryMessage);

    log.info("{} - Sending message, retryMessage: {}", LOG_DESC, retryMessage);
    rocketMqProducer.asyncSend(retryMessage.getTopic(), JsonUtil.toJson(retryMessage),
        CodeEnum.codeOf(RocketMQDelayLevelEnum.class, queryInitLevel).orElse(RocketMQDelayLevelEnum.FiveSeconds), LOG_DESC);
}

Determining whether to continue retrying:

public void sendDelayRetry(RetryMessage<?> retryMessage) {
    int currentCount;
    retryMessage.setCurrentCount(currentCount = retryMessage.getCurrentCount() + 1);
    // stop if max retries reached
    if (currentCount > retryMessage.getTotalCount()) {
        log.warn("{} - Max retries reached {}, stopping retry! retryMessage: {}", retryMessage.getEventDesc(), retryMessage.getTotalCount(), JsonUtil.toJson(retryMessage));
        return;
    }
    log.info("{} - Sending retry message-{}/{}, retryMessage: {}", retryMessage.getEventDesc(), retryMessage.getCurrentCount(), retryMessage.getTotalCount(), JsonUtil.toJson(retryMessage));
    int delayLevel = Integer.parseInt(retryMessage.getDelayLevels().split(",")[retryMessage.getCurrentCount() - 1]);
    rocketMqProducer.asyncSend(retryMessage.getTopic(), retryMessage,
        CodeEnum.codeOf(RocketMQDelayLevelEnum.class, delayLevel).orElse(RocketMQDelayLevelEnum.FiveSeconds), retryMessage.getEventDesc() + ", sending retry message");
}

3. Fault‑Tolerant Notification to Upstream Systems

If a callback to an upstream system fails (network glitch or short‑term outage), simple retries may not suffice. The solution reuses the delayed‑retry framework to periodically resend notifications until they succeed.

Design Pattern Applications in the Payment Center

Template Method

The template‑method pattern defines the skeleton of an algorithm while allowing subclasses to override specific steps. In the payment domain, all payment products follow the same overall flow, so a template can encapsulate the common steps and let each product implement its own variations.

Strategy

The strategy pattern encapsulates a family of algorithms and makes them interchangeable. For active payment queries, different channels (Alipay, WeChat, UnionPay) require distinct query logic; each can be implemented as a separate strategy class and selected at runtime.

Two strategy interfaces are defined: callChannel assembles query parameters and calls the third‑party, while execute normalizes the response to the payment‑center status. In Spring, all strategy beans are loaded into a map at startup for quick lookup.

@Component
public class PayQueryStrategyContext {
    private final Map<String, PayQueryStrategy> payQueryStrategyMap = Maps.newConcurrentMap();

    public PayQueryStrategyContext(Map<String, PayQueryStrategy> payQueryStrategyMap) {
        this.payQueryStrategyMap.clear();
        payQueryStrategyMap.forEach(this.payQueryStrategyMap::put);
    }

    public PayQueryStrategy getPayQuery(@NotNull String channelCode) {
        return this.payQueryStrategyMap.get(OperationTypeConst.Pay_Query + channelCode);
    }
}

The article also promotes a comprehensive video tutorial series (≈40 hours, 100 episodes) covering the entire mall project built with SpringBoot 2.7, Docker deployment, and more.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

RocketMQpaymentdesign-patterns
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.