Wallet and Payment Module Interaction
The Wallet module is mainly responsible to handle all business flows which are involving emoney, but also contains other wallet-related flows.
The Payment module is responsible to implement the flows required to process payment transactions transferring funds in and out of the emoney realm via the payment gateways, and to offer payment method and payment instrument handling.
The term "Payment Service" is used both in Wallet and Payment Module. In the Wallet module, the Payment Service is responsible to implement the business processes to allow users / the system to increase (Incoming/Funding) or decrease (Outgoing/Withdrawal) wallet account balances (WPayment Service). In the Payment Module, the Payment Service is responsible to handle interactions with the gateways (PPayment Service) via the transaction processors.
The term "payment" is used in the Wallet Module to represent a process that increases or decreases balance of wallet accounts, where in the Payment Module we usually talk about "payment transactions", which are representing the status of an interaction with a PSP / of a transaction within a PSP.
High level process
The key point to understand payment actions and callbacks is to think about module transitions. Ignoring the details, the process is simple:
- Based on the business transaction needs, the Wallet module instructs the Payment Module to perform a payment transaction directly or via payment action (blue arrows).
- The Payment module notifies the Wallet module about a change on a payment transaction, and the Wallet module will process this change via a payment callback (red arrows).
Other modules could be interacting with the Payment module in the same way.
Invoking the Payment module
There are currently two possible ways to instruct the Payment module to process a payment transaction:
- A wallet service calling the payment session service commit, where the session service may invoke the PPayment Service directly (dark blue arrows)
- A wallet service creating a payment action entry in the database, which will be processed by a wallet module job and then invokes the PPayment Service without a session (light blue arrows),
The intention of the payment session service is mainly to improve the consumer experience during any payment flow where the user is going through, allowing payment method overview and payment instrument handling. Related to this, a payment session commit might directly be routed to the responsible gateway, without a payment/action job in between. It is done in cases where a direct feedback of the gateway results is required for the consumer (nicer UX, avoid storing critical data like CVV, support OTP flows, ...), and if there are no approval processes possible. That is basically only true for incoming money (Real world->Wallet perspective).
The intention of the Payment Action is to improve performance and to decouple the API call response time of the related business process from the external PSP invocation. Business(wallet) services will only create a representing database entry, and processing this payment action will be done by a job. During payment action processing, the Wallet module directly calls the PPayment Service without Payment Session Service, because there is no user interaction which would require any further input.
Handling payment transaction changes
In both cases, after the gateway invocation in the Payment module has been processed, there will be a call to the payment transaction publisher to publish the payment transaction change with the status provided by the gateway (success/pending/failure/unknown) to all registered payment transaction listeners. The default payment transaction listener implementation in the Wallet module is using job based asynchronous processing, like for the payment actions. Product implementations are supposed to do the same. That means, instead of directly triggering the business processes in the Wallet module to react on the payment transaction change, there will be a new payment callback written to the database. A job will pick up the pending callbacks and process them using the according listener implementation. The mentioned business logic will usually be some sort of matching algorithm, to assign the payment transaction to the correct business object (a wallet, a purchase,...) and apply the logic of the related process based on the payment transaction status (e. g. create a Payment of type Incoming to credit a wallet account in case of a Purchase and payment transaction status "success").
Diagram with flow description
1.1 Client invokes an API which is implementing a business transaction service which requires a payment transaction
1.2. Client invokes the API of the WPaymentService directly (e. g. Funding/Withdrawal)
2.1 Both business transaction and payment service need to commit the payment session if used
2.2 In case there is no payment session commit with a direct PPayment Service call, or no payment session at all, a payment action will be created
3.1 In case a payment action has been used, the processing job will invoke the payment action processor to also call the PPayment Service operation as defined by the payment action
3.2 In case a payment session has been committed and it is incoming money, the respective operation of the PPayment Session will be called (Auth/capture/..)
4. Internal payment transaction routing to the responsible payment transaction processor containing the transaction and gateway information
5. Processor delegates to the payment gateway
6. Processor calls payment transaction publisher to inform about the received status from the gateway
7. Publisher calls all registered payment transaction listeners
8. Payment transaction listener impl in Wallet module creates a payment callback db entry
9. Payment callback job reads pending payment callbacks and invokes processing
10. Wallet payment transaction listener delegates to the payment transaction change handler. These handler are responsible to implement the actual business logic that will finish (or fail) the business transaction.
Asynchronous payment execution mode
Since CoreWallet release 1.2.58 the Payment module and Wallet module support asynchronous payment execution for Payment.confirm and Purchase.confirm, both of which commit the payment-session, which leads to payment-execution.
The main benefits of asynchronous payment execution are the following:
- Better scalability: no database connection is used while waiting for a payment-gateway result (which typically involves invoking a remote service, such as a PSP)
- Better PSP throttling: there is a limited number of threads responsible for payment-gateway invocation, thus preventing overloading a payment-gateway
- Better robustness: misbehaving remote services (e.g. taking >5 seconds to respond) have a limited impact on the CoreWallet
- At most the maximum number of async threads is occupied (plus any waiting API request thread)
- No database connections are occupied
The main disadvantages are the following:
- There are more database interactions, because what used to be one database transaction is now a couple of separate database transactions. However, database latency is usually much lower than PSP latency, and can be more strictly controlled.
- During payment-execution, there is no exclusive database-lock on the entity anymore, thus race-conditions must be handled differently (e.g. a new entity state).
- There are a couple of additional border-cases to consider, e.g. in case of a JVM-crash during payment-execution. However, this is true for synchronous payment-execution as well, where the whole database transaction would roll back, basically ignoring the border-case completely.
The following methods have an execution-mode parameter for either sync or async execution:
The asynchronous executor can be configured using the following properties:
trimplement.wallet.server.payment.impl.async.taskExecutor.maxPoolSize- to define the maximum number of threads handling asynchronous payment execution; defaults to 20
trimplement.wallet.server.payment.impl.async.taskExecutor.queueCapacity- to define the maximum number of async-tasks in the queue waiting to be executed; defaults to 1000
Should all async execution threads be occupied, then tasks are put into a task-executor-queue. Should the queue be full, then the payment-execution is rejected/failed directly.
The Payment.confirm / Purchase.confirm are typically structured as follows:
- Prepare in database transaction
- Set entity status to ConfirmInProgress to prevent parallel operations
- Call PaymentSession.commit to start execution in async mode
- Wait for payment-execution-completion without database transaction
- Process payment-execution-result in database transaction
- On success, update entity-status to Confirmed
- On error, update entity-status to Created
When a Product provides their own implementation for Payment/Purchase.confirm, they should observe the following:
- Do not wrap the whole method in a database transaction
- The method should follow the same principles as stated above regarding prepare / call / process result
When a Product provides a PaymentGateway implementation, it should observe the following:
- It should not assume that any database transaction exists during a PaymentGateway call
- It can interact with the database in its own database transaction, but it should not do remote communication while a database transaction is active
Following these principles ensure a stable and scalable system, even with limited database connections.
For example, a load-test with 75 concurrent users, a PSP-response-time of 5 seconds, and 20 async-payment-threads resulted in at most 14 used database connections, because none of the waiting occupied any database connection.