Go in Practice: How to Elegantly Integrate International Payments (PayPal) and a Dynamic Exchange Rate System?
When expanding into international markets, integrating international payment solutions (such as PayPal and Stripe) is essential. Unlike the unified domestic payment environment, the first challenge businesses face with international payments is multiple currencies and exchange rate fluctuations.
I. Why Have a Floating Exchange Rate?
At first, we hard-coded a fixed exchange rate (such as 7.2) into the code—a simple, brute-force approach. But we soon encountered the following issues in production:
- Exchange rate fluctuations have caused a discrepancy between the actual amount received and the financial budget.
- The amount users see at checkout does not match the actual amount charged, resulting in a poor user experience and even refunds.
- During financial reconciliation, we discovered a large number of exchange rate differences that were difficult to reconcile.
Clearly, we need to integrate a real-time exchange rate API to calculate prices dynamically.
II. Design of the Exchange Rate Retrieval Module
We have developed a standalone currency conversion toolkit whose primary function is to retrieve real-time exchange rates and handle exceptions.
2.1 Exchange Rate Data Structure
go
type ExchangeRate struct {
Result string `json:"result"`
BaseCode string `json:"base_code"` // Base currency (e.g., USD)
TargetCode string `json:"target_code"` // Target currency (e.g., CNY)
ConversionRate float64 `json:"conversion_rate"` // Key: Real-time exchange rate
ConversionResult float64 `json:"conversion_result"`
TimeLastUpdateUnix int `json:"time_last_update_unix"`
}
2.2 Core Implementation for Retrieving Exchange Rates
The core function is responsible for calling third-party APIs, and we have implemented several layers of robustness safeguards:
- Timeout Control: Set a 10-second timeout to prevent requests from hanging.
- Status Code Check: Ensure the response is normal.
- Wrapper: Add contextual information to each layer of the error to facilitate troubleshooting.
go
func GetExchangeRate(baseCode, targetCode string, amount float64) (*ExchangeRate, error) {
// In a production environment, it is recommended to store the API key in a configuration file
apiKey := "YOUR_API_KEY"
url := fmt.Sprintf("https://v6.exchangerate-api.com/v6/%s/pair/%s/%s", apiKey, baseCode, targetCode)
// Create a client with a timeout
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(url)
if err != nil {
return nil, fmt.Errorf("Failed to request the exchange rate API: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Exception in exchange rate API response status code: %d", resp.StatusCode)
}
var result ExchangeRate
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("Failed to parse the exchange rate JSON: %w", err)
}
return &result, nil
}
[!TIP]
Best Practice: We recommend caching exchange rates in Redis to avoid making a request to a third-party API for every payment. This will not only speed up order response times but also save on API call quotas.
III. Internationalization Design of the Order Model
To support multiple currencies, the order table must record the exchange rate at the time the order was placed and the converted amounts in each currency. Our GORM model is designed as follows:
go
type OrderModel struct {
OrderNo string `gorm:"size:64;uniqueIndex"` // Order number
TotalPrice float64 `gorm:"type:decimal(10,2)"` // Original price as seen by the user (RMB)
// Key fields for international payments
ExchangeRate float64 `gorm:"type:decimal(10,4)"` // Snapshot of the exchange rate at that time (rounded to 4 decimal places)
PaypalAmount float64 `gorm:"type:decimal(10,2)"` // Converted amount in U.S. dollars
// Payment Channel Information
PaymentMethod string // Payment method: PayPal, Alipay, WeChat
PaypalOrderID string // Payment gateway order number
Status int // 1-Pending Payment 2-Paid 3-Canceled
}
Design Highlights
- Exchange Rate Snapshot: Records the exchange rate at the time the order was placed to prevent subsequent exchange rate fluctuations from causing reconciliation issues for the finance department.
- Dual Currency Storage: Both the original local currency amount (CNY) and the converted foreign currency amount (USD) are saved simultaneously, facilitating multi-dimensional financial analysis.
- Expansion of Payment Methods: Reserve the
PaymentMethodto support future integration with other channels such as Stripe and Wise.
IV. Practical Guide to Integrating PayPal Payments
The core logic flow for setting up PayPal payments is as follows:
go
func CreatePaypalPayment(orderNo string) (string, error) {
// 1. Query order information
var order OrderModel
if err := db.Where("order_no = ?", orderNo).First(&order).Error; err != nil {
return "", fmt.Errorf("Order does not exist")
}
// 2. Retrieve real-time exchange rates and convert
rate := GetCurrentRate() // Read from the Redis cache
usdAmount := order.TotalPrice / rate // Price in RMB / exchange rate = Amount in USD
// 3. Create a PayPal order
paypalOrder, err := paypalUtil.CreateOrder(CreateOrderParams{
OrderNo: orderNo,
Amount: usdAmount,
Currency: "USD",
Description: generateDescription(order),
})
if err != nil {
return "", fmt.Errorf("Failed to create a PayPal order: %w", err)
}
// 4. Save payment information to the database
updates := map[string]any{
"payment_method": "paypal",
"paypal_order_id": paypalOrder.ID,
"exchange_rate": rate,
"paypal_amount": usdAmount,
}
if err := db.Model(&order).Updates(updates).Error; err != nil {
return "", fmt.Errorf("Failed to update order payment information: %w", err)
}
// 5. Return the payment redirect link to the front end
return paypalUtil.GetApprovalURL(paypalOrder), nil
}
Key Details
- When Exchange Rates Are Retrieved: Real-time exchange rates are retrieved only when the payment is created, ensuring that the final payment amount displayed to the user is as accurate as possible.
- Numeric Precision: Database fields are stored using
decimal(10,2)to avoid potential loss of precision that can occur with Go floating-point operations. - Transaction Protection: When updating payment status through interaction with the gateway, a status lock must be applied to prevent duplicate payments caused by concurrent retries.
V. Summary of Pitfalls
5.1 Exchange Rate Caching Strategy
At first, every time we initiated a payment, we made a real-time call to a third-party API, which not only resulted in slow response times but also frequently triggered rate-limiting errors. We’ve now switched to caching exchange rates in Redis every 12 hours, which ensures real-time accuracy while keeping call costs under control.
5.2 Handling of Abnormal Exchange Rate Downgrades
Third-party APIs may experience network timeouts or service unavailability. We have designed a disaster recovery and fallback mechanism: if an API retrieval fails, the system automatically uses the most recent successfully cached exchange rate and immediately sends an alert to the backend.
5.3 Concurrency and Settlement Consistency
When multiple users place orders simultaneously, exchange rates may fluctuate due to slight time differences. Our solution is to record a snapshot of the exchange rate for each order at the exact moment it is placed, and all subsequent payments and reconciliations are based on that snapshot.
5.4 Sandbox Testing Environment
During the development and testing phases, frequent calls to the live exchange rate API can significantly deplete your quota. We recommend deploying a mock exchange rate service in the development environment that returns fixed values or values that fluctuate within a narrow range.
VI. Further Reflections
This approach works not only for PayPal, but also for Stripe and Wise—the process is the same:
- Centralized Exchange Rate Access Point: The business layer does not concern itself with specific exchange rate sources; all rates are retrieved from a centralized service or cache.
- Order Exchange Rate Snapshot: Regardless of which payment gateway is used, an exchange rate snapshot is recorded in the order when the payment is created.
- Separation of Responsibilities for Payment Gateways: Payment gateways only process payments; they do not participate in exchange rate calculations.
Technology Stack Reference
- Go 1.21 + Gin (backend service framework)
- PostgreSQL + GORM (Data Storage and ORM)
- Redis (Exchange Rate Cache)
- PayPal REST API (payment gateway)
- exchangerate-api.com (currency exchange rate source)
[!NOTE]
Copyright Notice: This article is intended as a discussion of concepts and includes pseudocode. When implementing this in a production environment, be sure to adapt it to your specific business context and compliance requirements.