Go Architecture Patterns Series: ← Previous: Saga Pattern | Series Overview
Introduction
Building a robust payment gateway integration is one of the most critical components of any e-commerce or financial application. Payment systems must handle multiple providers, ensure transactional integrity, implement retry mechanisms, support scheduled payments, and maintain comprehensive audit trails.
In this guide, we’ll explore how to build a production-ready payment gateway integration system in Go that handles:
- Multiple Payment Providers: Stripe, PayPal, Square, and custom gateways
- Transaction Management: Atomic operations with proper rollback
- Retry Logic: Exponential backoff and idempotency
- Scheduled Payments: Recurring billing and delayed charges
- Data Persistence: Both SQL and NoSQL approaches
- Security: PCI compliance and sensitive data handling
Architecture Overview
Our payment system follows the Strategy pattern to support multiple payment gateways while maintaining a consistent interface.
Transaction Flow
Understanding the complete payment transaction flow is crucial for building reliable systems.
Retry Mechanism Flow
Project Structure
payment-gateway/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── gateway/
│ │ ├── interface.go
│ │ ├── stripe.go
│ │ ├── paypal.go
│ │ └── factory.go
│ ├── service/
│ │ ├── payment.go
│ │ ├── transaction.go
│ │ ├── retry.go
│ │ └── scheduler.go
│ ├── models/
│ │ ├── payment.go
│ │ └── transaction.go
│ ├── repository/
│ │ ├── sql/
│ │ │ └── transaction_repo.go
│ │ └── nosql/
│ │ └── transaction_repo.go
│ └── middleware/
│ ├── idempotency.go
│ └── audit.go
├── migrations/
│ ├── sql/
│ │ └── 001_create_tables.sql
│ └── nosql/
│ └── schema.json
└── go.mod
Core Payment Gateway Interface
// internal/gateway/interface.go
package gateway
import (
"context"
"time"
)
// PaymentGateway defines the interface that all payment providers must implement
type PaymentGateway interface {
// ProcessPayment processes a payment transaction
ProcessPayment(ctx context.Context, req *PaymentRequest) (*PaymentResponse, error)
// RefundPayment refunds a previously processed payment
RefundPayment(ctx context.Context, transactionID string, amount int64) (*RefundResponse, error)
// GetTransaction retrieves transaction details from the gateway
GetTransaction(ctx context.Context, transactionID string) (*Transaction, error)
// VerifyWebhook verifies webhook signatures
VerifyWebhook(payload []byte, signature string) error
// Name returns the gateway provider name
Name() string
// SupportsRecurring indicates if the gateway supports recurring payments
SupportsRecurring() bool
// CreateCustomer creates a customer profile (for recurring payments)
CreateCustomer(ctx context.Context, req *CustomerRequest) (*Customer, error)
}
// PaymentRequest represents a payment request
type PaymentRequest struct {
Amount int64 // Amount in smallest currency unit (cents)
Currency string // ISO currency code (USD, EUR, etc.)
PaymentMethodID string // Payment method identifier
CustomerID string // Customer identifier
Description string // Payment description
IdempotencyKey string // Idempotency key for duplicate prevention
Metadata map[string]string // Additional metadata
CaptureMethod CaptureMethod // Automatic or manual capture
StatementDescriptor string // Description on customer's statement
}
// PaymentResponse represents a payment response from the gateway
type PaymentResponse struct {
TransactionID string
Status PaymentStatus
Amount int64
Currency string
GatewayResponse map[string]interface{}
ProcessedAt time.Time
Fee int64
NetAmount int64
}
// RefundResponse represents a refund response
type RefundResponse struct {
RefundID string
TransactionID string
Amount int64
Status RefundStatus
ProcessedAt time.Time
}
// Transaction represents gateway transaction details
type Transaction struct {
ID string
Amount int64
Currency string
Status PaymentStatus
CreatedAt time.Time
UpdatedAt time.Time
Metadata map[string]interface{}
}
// Customer represents a customer profile
type Customer struct {
ID string
Email string
PaymentMethods []string
CreatedAt time.Time
}
// CustomerRequest represents a customer creation request
type CustomerRequest struct {
Email string
Name string
Phone string
Metadata map[string]string
}
// PaymentStatus represents payment status
type PaymentStatus string
const (
PaymentStatusPending PaymentStatus = "pending"
PaymentStatusProcessing PaymentStatus = "processing"
PaymentStatusSuccess PaymentStatus = "succeeded"
PaymentStatusFailed PaymentStatus = "failed"
PaymentStatusCanceled PaymentStatus = "canceled"
PaymentStatusRefunded PaymentStatus = "refunded"
)
// RefundStatus represents refund status
type RefundStatus string
const (
RefundStatusPending RefundStatus = "pending"
RefundStatusSucceeded RefundStatus = "succeeded"
RefundStatusFailed RefundStatus = "failed"
)
// CaptureMethod defines when to capture payment
type CaptureMethod string
const (
CaptureMethodAutomatic CaptureMethod = "automatic"
CaptureMethodManual CaptureMethod = "manual"
)
Stripe Gateway Implementation
// internal/gateway/stripe.go
package gateway
import (
"context"
"fmt"
"time"
"github.com/stripe/stripe-go/v76"
"github.com/stripe/stripe-go/v76/client"
"github.com/stripe/stripe-go/v76/webhook"
)
// StripeGateway implements PaymentGateway for Stripe
type StripeGateway struct {
client *client.API
webhookSecret string
}
// NewStripeGateway creates a new Stripe gateway
func NewStripeGateway(apiKey, webhookSecret string) *StripeGateway {
sc := &client.API{}
sc.Init(apiKey, nil)
return &StripeGateway{
client: sc,
webhookSecret: webhookSecret,
}
}
// ProcessPayment processes a payment through Stripe
func (s *StripeGateway) ProcessPayment(ctx context.Context, req *PaymentRequest) (*PaymentResponse, error) {
params := &stripe.PaymentIntentParams{
Amount: stripe.Int64(req.Amount),
Currency: stripe.String(req.Currency),
PaymentMethod: stripe.String(req.PaymentMethodID),
Customer: stripe.String(req.CustomerID),
Description: stripe.String(req.Description),
Confirm: stripe.Bool(true),
}
// Set idempotency key
params.IdempotencyKey = stripe.String(req.IdempotencyKey)
// Add metadata
if len(req.Metadata) > 0 {
params.Metadata = req.Metadata
}
// Set capture method
if req.CaptureMethod == CaptureMethodManual {
params.CaptureMethod = stripe.String(string(stripe.PaymentIntentCaptureMethodManual))
}
// Set statement descriptor
if req.StatementDescriptor != "" {
params.StatementDescriptor = stripe.String(req.StatementDescriptor)
}
pi, err := s.client.PaymentIntents.New(params)
if err != nil {
return nil, fmt.Errorf("stripe payment failed: %w", err)
}
// Map Stripe status to our status
status := s.mapStripeStatus(pi.Status)
return &PaymentResponse{
TransactionID: pi.ID,
Status: status,
Amount: pi.Amount,
Currency: string(pi.Currency),
GatewayResponse: map[string]interface{}{
"stripe_payment_intent": pi,
},
ProcessedAt: time.Unix(pi.Created, 0),
Fee: s.calculateStripeFee(pi.Amount),
NetAmount: pi.Amount - s.calculateStripeFee(pi.Amount),
}, nil
}
// RefundPayment refunds a Stripe payment
func (s *StripeGateway) RefundPayment(ctx context.Context, transactionID string, amount int64) (*RefundResponse, error) {
params := &stripe.RefundParams{
PaymentIntent: stripe.String(transactionID),
}
if amount > 0 {
params.Amount = stripe.Int64(amount)
}
refund, err := s.client.Refunds.New(params)
if err != nil {
return nil, fmt.Errorf("stripe refund failed: %w", err)
}
return &RefundResponse{
RefundID: refund.ID,
TransactionID: refund.PaymentIntent.ID,
Amount: refund.Amount,
Status: s.mapRefundStatus(refund.Status),
ProcessedAt: time.Unix(refund.Created, 0),
}, nil
}
// GetTransaction retrieves transaction details
func (s *StripeGateway) GetTransaction(ctx context.Context, transactionID string) (*Transaction, error) {
pi, err := s.client.PaymentIntents.Get(transactionID, nil)
if err != nil {
return nil, fmt.Errorf("failed to get transaction: %w", err)
}
return &Transaction{
ID: pi.ID,
Amount: pi.Amount,
Currency: string(pi.Currency),
Status: s.mapStripeStatus(pi.Status),
CreatedAt: time.Unix(pi.Created, 0),
UpdatedAt: time.Now(),
Metadata: map[string]interface{}{
"description": pi.Description,
"customer": pi.Customer.ID,
},
}, nil
}
// VerifyWebhook verifies Stripe webhook signatures
func (s *StripeGateway) VerifyWebhook(payload []byte, signature string) error {
_, err := webhook.ConstructEvent(payload, signature, s.webhookSecret)
return err
}
// Name returns the gateway name
func (s *StripeGateway) Name() string {
return "stripe"
}
// SupportsRecurring indicates Stripe supports recurring payments
func (s *StripeGateway) SupportsRecurring() bool {
return true
}
// CreateCustomer creates a Stripe customer
func (s *StripeGateway) CreateCustomer(ctx context.Context, req *CustomerRequest) (*Customer, error) {
params := &stripe.CustomerParams{
Email: stripe.String(req.Email),
Name: stripe.String(req.Name),
Phone: stripe.String(req.Phone),
}
if len(req.Metadata) > 0 {
params.Metadata = req.Metadata
}
customer, err := s.client.Customers.New(params)
if err != nil {
return nil, fmt.Errorf("failed to create customer: %w", err)
}
return &Customer{
ID: customer.ID,
Email: customer.Email,
CreatedAt: time.Unix(customer.Created, 0),
}, nil
}
// mapStripeStatus maps Stripe status to our internal status
func (s *StripeGateway) mapStripeStatus(status stripe.PaymentIntentStatus) PaymentStatus {
switch status {
case stripe.PaymentIntentStatusSucceeded:
return PaymentStatusSuccess
case stripe.PaymentIntentStatusProcessing:
return PaymentStatusProcessing
case stripe.PaymentIntentStatusRequiresPaymentMethod,
stripe.PaymentIntentStatusRequiresConfirmation,
stripe.PaymentIntentStatusRequiresAction:
return PaymentStatusPending
case stripe.PaymentIntentStatusCanceled:
return PaymentStatusCanceled
default:
return PaymentStatusFailed
}
}
// mapRefundStatus maps Stripe refund status
func (s *StripeGateway) mapRefundStatus(status stripe.RefundStatus) RefundStatus {
switch status {
case stripe.RefundStatusSucceeded:
return RefundStatusSucceeded
case stripe.RefundStatusPending:
return RefundStatusPending
default:
return RefundStatusFailed
}
}
// calculateStripeFee calculates Stripe's fee (2.9% + 30¢)
func (s *StripeGateway) calculateStripeFee(amount int64) int64 {
return (amount*29)/1000 + 30
}
PayPal Gateway Implementation
// internal/gateway/paypal.go
package gateway
import (
"context"
"fmt"
"time"
)
// PayPalGateway implements PaymentGateway for PayPal
type PayPalGateway struct {
clientID string
clientSecret string
baseURL string
accessToken string
tokenExpiry time.Time
}
// NewPayPalGateway creates a new PayPal gateway
func NewPayPalGateway(clientID, clientSecret string, sandbox bool) *PayPalGateway {
baseURL := "https://api.paypal.com"
if sandbox {
baseURL = "https://api.sandbox.paypal.com"
}
return &PayPalGateway{
clientID: clientID,
clientSecret: clientSecret,
baseURL: baseURL,
}
}
// ProcessPayment processes a payment through PayPal
func (p *PayPalGateway) ProcessPayment(ctx context.Context, req *PaymentRequest) (*PaymentResponse, error) {
// Ensure we have a valid access token
if err := p.ensureAccessToken(ctx); err != nil {
return nil, fmt.Errorf("failed to get access token: %w", err)
}
// Create PayPal order
orderReq := map[string]interface{}{
"intent": "CAPTURE",
"purchase_units": []map[string]interface{}{
{
"amount": map[string]interface{}{
"currency_code": req.Currency,
"value": fmt.Sprintf("%.2f", float64(req.Amount)/100),
},
"description": req.Description,
},
},
"payment_source": map[string]interface{}{
"paypal": map[string]interface{}{
"experience_context": map[string]interface{}{
"payment_method_preference": "IMMEDIATE_PAYMENT_REQUIRED",
},
},
},
}
// In a real implementation, you would make HTTP requests to PayPal API
// For brevity, we'll simulate the response
return &PaymentResponse{
TransactionID: generateTransactionID("paypal"),
Status: PaymentStatusSuccess,
Amount: req.Amount,
Currency: req.Currency,
GatewayResponse: map[string]interface{}{
"paypal_order_id": "PAYPAL-ORDER-123",
},
ProcessedAt: time.Now(),
Fee: p.calculatePayPalFee(req.Amount),
NetAmount: req.Amount - p.calculatePayPalFee(req.Amount),
}, nil
}
// RefundPayment refunds a PayPal payment
func (p *PayPalGateway) RefundPayment(ctx context.Context, transactionID string, amount int64) (*RefundResponse, error) {
if err := p.ensureAccessToken(ctx); err != nil {
return nil, fmt.Errorf("failed to get access token: %w", err)
}
// Simulate PayPal refund
return &RefundResponse{
RefundID: generateTransactionID("refund"),
TransactionID: transactionID,
Amount: amount,
Status: RefundStatusSucceeded,
ProcessedAt: time.Now(),
}, nil
}
// GetTransaction retrieves transaction details from PayPal
func (p *PayPalGateway) GetTransaction(ctx context.Context, transactionID string) (*Transaction, error) {
if err := p.ensureAccessToken(ctx); err != nil {
return nil, fmt.Errorf("failed to get access token: %w", err)
}
// Simulate transaction retrieval
return &Transaction{
ID: transactionID,
Amount: 10000,
Currency: "USD",
Status: PaymentStatusSuccess,
CreatedAt: time.Now().Add(-1 * time.Hour),
UpdatedAt: time.Now(),
}, nil
}
// VerifyWebhook verifies PayPal webhook signatures
func (p *PayPalGateway) VerifyWebhook(payload []byte, signature string) error {
// Implement PayPal webhook verification
return nil
}
// Name returns the gateway name
func (p *PayPalGateway) Name() string {
return "paypal"
}
// SupportsRecurring indicates PayPal supports recurring payments
func (p *PayPalGateway) SupportsRecurring() bool {
return true
}
// CreateCustomer creates a PayPal customer (vault)
func (p *PayPalGateway) CreateCustomer(ctx context.Context, req *CustomerRequest) (*Customer, error) {
if err := p.ensureAccessToken(ctx); err != nil {
return nil, fmt.Errorf("failed to get access token: %w", err)
}
return &Customer{
ID: generateTransactionID("cust"),
Email: req.Email,
CreatedAt: time.Now(),
}, nil
}
// ensureAccessToken ensures we have a valid access token
func (p *PayPalGateway) ensureAccessToken(ctx context.Context) error {
if time.Now().Before(p.tokenExpiry) {
return nil
}
// In real implementation, make OAuth2 token request to PayPal
p.accessToken = "simulated-access-token"
p.tokenExpiry = time.Now().Add(8 * time.Hour)
return nil
}
// calculatePayPalFee calculates PayPal's fee (2.9% + 30¢)
func (p *PayPalGateway) calculatePayPalFee(amount int64) int64 {
return (amount*29)/1000 + 30
}
// generateTransactionID generates a transaction ID
func generateTransactionID(prefix string) string {
return fmt.Sprintf("%s_%d", prefix, time.Now().UnixNano())
}
Gateway Factory
// internal/gateway/factory.go
package gateway
import (
"fmt"
)
// GatewayType represents the type of payment gateway
type GatewayType string
const (
GatewayTypeStripe GatewayType = "stripe"
GatewayTypePayPal GatewayType = "paypal"
GatewayTypeSquare GatewayType = "square"
)
// GatewayFactory creates payment gateway instances
type GatewayFactory struct {
gateways map[GatewayType]PaymentGateway
}
// NewGatewayFactory creates a new gateway factory
func NewGatewayFactory() *GatewayFactory {
return &GatewayFactory{
gateways: make(map[GatewayType]PaymentGateway),
}
}
// RegisterGateway registers a payment gateway
func (f *GatewayFactory) RegisterGateway(gatewayType GatewayType, gateway PaymentGateway) {
f.gateways[gatewayType] = gateway
}
// GetGateway retrieves a payment gateway by type
func (f *GatewayFactory) GetGateway(gatewayType GatewayType) (PaymentGateway, error) {
gateway, exists := f.gateways[gatewayType]
if !exists {
return nil, fmt.Errorf("gateway not found: %s", gatewayType)
}
return gateway, nil
}
// ListGateways returns all registered gateways
func (f *GatewayFactory) ListGateways() []string {
names := make([]string, 0, len(f.gateways))
for _, gateway := range f.gateways {
names = append(names, gateway.Name())
}
return names
}
Transaction Service with Retry Logic
// internal/service/transaction.go
package service
import (
"context"
"database/sql"
"fmt"
"time"
"payment-gateway/internal/gateway"
"payment-gateway/internal/models"
)
// TransactionService handles payment transactions
type TransactionService struct {
db *sql.DB
gatewayFactory *gateway.GatewayFactory
retryService *RetryService
}
// NewTransactionService creates a new transaction service
func NewTransactionService(
db *sql.DB,
gatewayFactory *gateway.GatewayFactory,
retryService *RetryService,
) *TransactionService {
return &TransactionService{
db: db,
gatewayFactory: gatewayFactory,
retryService: retryService,
}
}
// ProcessPayment processes a payment with retry logic
func (s *TransactionService) ProcessPayment(
ctx context.Context,
req *models.PaymentRequest,
) (*models.Transaction, error) {
// Start database transaction
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
if err != nil {
return nil, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()
// Create transaction record
transaction := &models.Transaction{
ID: generateID(),
Amount: req.Amount,
Currency: req.Currency,
Status: models.TransactionStatusPending,
GatewayType: req.GatewayType,
CustomerID: req.CustomerID,
PaymentMethodID: req.PaymentMethodID,
IdempotencyKey: req.IdempotencyKey,
Metadata: req.Metadata,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
RetryCount: 0,
MaxRetries: 3,
}
// Save initial transaction
if err := s.saveTransaction(ctx, tx, transaction); err != nil {
return nil, fmt.Errorf("failed to save transaction: %w", err)
}
// Commit the initial save
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("failed to commit transaction: %w", err)
}
// Process payment with retry logic
err = s.retryService.ExecuteWithRetry(ctx, func(ctx context.Context) error {
return s.processPaymentAttempt(ctx, transaction, req)
})
if err != nil {
transaction.Status = models.TransactionStatusFailed
transaction.ErrorMessage = err.Error()
s.updateTransactionStatus(ctx, transaction)
return nil, fmt.Errorf("payment failed after retries: %w", err)
}
return transaction, nil
}
// processPaymentAttempt attempts to process a payment
func (s *TransactionService) processPaymentAttempt(
ctx context.Context,
transaction *models.Transaction,
req *models.PaymentRequest,
) error {
// Get the appropriate gateway
gw, err := s.gatewayFactory.GetGateway(gateway.GatewayType(req.GatewayType))
if err != nil {
return fmt.Errorf("failed to get gateway: %w", err)
}
// Update status to processing
transaction.Status = models.TransactionStatusProcessing
transaction.UpdatedAt = time.Now()
if err := s.updateTransactionStatus(ctx, transaction); err != nil {
return fmt.Errorf("failed to update transaction status: %w", err)
}
// Create gateway payment request
gatewayReq := &gateway.PaymentRequest{
Amount: req.Amount,
Currency: req.Currency,
PaymentMethodID: req.PaymentMethodID,
CustomerID: req.CustomerID,
Description: req.Description,
IdempotencyKey: req.IdempotencyKey,
Metadata: req.Metadata,
CaptureMethod: gateway.CaptureMethodAutomatic,
StatementDescriptor: "PAYMENT",
}
// Process payment through gateway
resp, err := gw.ProcessPayment(ctx, gatewayReq)
if err != nil {
transaction.RetryCount++
return err
}
// Update transaction with gateway response
transaction.GatewayTransactionID = resp.TransactionID
transaction.Status = s.mapGatewayStatus(resp.Status)
transaction.Fee = resp.Fee
transaction.NetAmount = resp.NetAmount
transaction.ProcessedAt = &resp.ProcessedAt
transaction.UpdatedAt = time.Now()
transaction.GatewayResponse = resp.GatewayResponse
if err := s.updateTransactionStatus(ctx, transaction); err != nil {
return fmt.Errorf("failed to update transaction: %w", err)
}
// Create audit log
s.createAuditLog(ctx, transaction, "payment_processed")
return nil
}
// saveTransaction saves a transaction to the database
func (s *TransactionService) saveTransaction(
ctx context.Context,
tx *sql.Tx,
transaction *models.Transaction,
) error {
query := `
INSERT INTO transactions (
id, amount, currency, status, gateway_type, customer_id,
payment_method_id, idempotency_key, created_at, updated_at,
retry_count, max_retries
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
`
_, err := tx.ExecContext(ctx, query,
transaction.ID,
transaction.Amount,
transaction.Currency,
transaction.Status,
transaction.GatewayType,
transaction.CustomerID,
transaction.PaymentMethodID,
transaction.IdempotencyKey,
transaction.CreatedAt,
transaction.UpdatedAt,
transaction.RetryCount,
transaction.MaxRetries,
)
return err
}
// updateTransactionStatus updates transaction status
func (s *TransactionService) updateTransactionStatus(
ctx context.Context,
transaction *models.Transaction,
) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
query := `
UPDATE transactions
SET status = $1,
gateway_transaction_id = $2,
fee = $3,
net_amount = $4,
processed_at = $5,
updated_at = $6,
retry_count = $7,
error_message = $8
WHERE id = $9
`
_, err = tx.ExecContext(ctx, query,
transaction.Status,
transaction.GatewayTransactionID,
transaction.Fee,
transaction.NetAmount,
transaction.ProcessedAt,
transaction.UpdatedAt,
transaction.RetryCount,
transaction.ErrorMessage,
transaction.ID,
)
if err != nil {
return err
}
return tx.Commit()
}
// createAuditLog creates an audit log entry
func (s *TransactionService) createAuditLog(
ctx context.Context,
transaction *models.Transaction,
action string,
) error {
query := `
INSERT INTO audit_logs (
id, transaction_id, action, status, created_at, metadata
) VALUES ($1, $2, $3, $4, $5, $6)
`
_, err := s.db.ExecContext(ctx, query,
generateID(),
transaction.ID,
action,
transaction.Status,
time.Now(),
transaction.Metadata,
)
return err
}
// mapGatewayStatus maps gateway status to transaction status
func (s *TransactionService) mapGatewayStatus(status gateway.PaymentStatus) models.TransactionStatus {
switch status {
case gateway.PaymentStatusSuccess:
return models.TransactionStatusSuccess
case gateway.PaymentStatusProcessing:
return models.TransactionStatusProcessing
case gateway.PaymentStatusPending:
return models.TransactionStatusPending
case gateway.PaymentStatusFailed:
return models.TransactionStatusFailed
default:
return models.TransactionStatusFailed
}
}
func generateID() string {
return fmt.Sprintf("txn_%d", time.Now().UnixNano())
}
Retry Service with Exponential Backoff
// internal/service/retry.go
package service
import (
"context"
"fmt"
"math"
"time"
)
// RetryService handles retry logic with exponential backoff
type RetryService struct {
maxRetries int
initialBackoff time.Duration
maxBackoff time.Duration
multiplier float64
}
// NewRetryService creates a new retry service
func NewRetryService() *RetryService {
return &RetryService{
maxRetries: 3,
initialBackoff: 2 * time.Second,
maxBackoff: 30 * time.Second,
multiplier: 2.0,
}
}
// ExecuteWithRetry executes a function with retry logic
func (r *RetryService) ExecuteWithRetry(
ctx context.Context,
fn func(context.Context) error,
) error {
var lastErr error
for attempt := 0; attempt <= r.maxRetries; attempt++ {
// Execute the function
err := fn(ctx)
if err == nil {
return nil
}
lastErr = err
// Don't retry on last attempt
if attempt == r.maxRetries {
break
}
// Check if error is retryable
if !r.isRetryable(err) {
return err
}
// Calculate backoff duration
backoff := r.calculateBackoff(attempt)
// Wait before retry
select {
case <-time.After(backoff):
// Continue to next attempt
case <-ctx.Done():
return ctx.Err()
}
}
return fmt.Errorf("max retries exceeded: %w", lastErr)
}
// calculateBackoff calculates exponential backoff duration
func (r *RetryService) calculateBackoff(attempt int) time.Duration {
backoff := float64(r.initialBackoff) * math.Pow(r.multiplier, float64(attempt))
if backoff > float64(r.maxBackoff) {
backoff = float64(r.maxBackoff)
}
return time.Duration(backoff)
}
// isRetryable determines if an error is retryable
func (r *RetryService) isRetryable(err error) bool {
// In a real implementation, check for specific error types
// For now, retry on all errors
return true
}
// SetMaxRetries sets the maximum number of retries
func (r *RetryService) SetMaxRetries(max int) {
r.maxRetries = max
}
// SetBackoffParams sets backoff parameters
func (r *RetryService) SetBackoffParams(initial, max time.Duration, multiplier float64) {
r.initialBackoff = initial
r.maxBackoff = max
r.multiplier = multiplier
}
Scheduler Service for Recurring Payments
// internal/service/scheduler.go
package service
import (
"context"
"database/sql"
"fmt"
"time"
"payment-gateway/internal/models"
)
// SchedulerService handles scheduled and recurring payments
type SchedulerService struct {
db *sql.DB
transactionService *TransactionService
}
// NewSchedulerService creates a new scheduler service
func NewSchedulerService(db *sql.DB, transactionService *TransactionService) *SchedulerService {
return &SchedulerService{
db: db,
transactionService: transactionService,
}
}
// CreateScheduledPayment creates a scheduled payment
func (s *SchedulerService) CreateScheduledPayment(
ctx context.Context,
req *models.ScheduledPaymentRequest,
) (*models.ScheduledPayment, error) {
scheduled := &models.ScheduledPayment{
ID: generateID(),
CustomerID: req.CustomerID,
Amount: req.Amount,
Currency: req.Currency,
GatewayType: req.GatewayType,
PaymentMethodID: req.PaymentMethodID,
ScheduleType: req.ScheduleType,
Interval: req.Interval,
StartDate: req.StartDate,
NextRunDate: req.StartDate,
Status: models.ScheduleStatusActive,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if req.EndDate != nil {
scheduled.EndDate = req.EndDate
}
query := `
INSERT INTO scheduled_payments (
id, customer_id, amount, currency, gateway_type,
payment_method_id, schedule_type, interval_value,
start_date, end_date, next_run_date, status,
created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
`
_, err := s.db.ExecContext(ctx, query,
scheduled.ID,
scheduled.CustomerID,
scheduled.Amount,
scheduled.Currency,
scheduled.GatewayType,
scheduled.PaymentMethodID,
scheduled.ScheduleType,
scheduled.Interval,
scheduled.StartDate,
scheduled.EndDate,
scheduled.NextRunDate,
scheduled.Status,
scheduled.CreatedAt,
scheduled.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to create scheduled payment: %w", err)
}
return scheduled, nil
}
// ProcessDuePayments processes all due scheduled payments
func (s *SchedulerService) ProcessDuePayments(ctx context.Context) error {
// Get all due payments
query := `
SELECT id, customer_id, amount, currency, gateway_type,
payment_method_id, schedule_type, interval_value,
next_run_date, end_date
FROM scheduled_payments
WHERE status = $1 AND next_run_date <= $2
`
rows, err := s.db.QueryContext(ctx, query,
models.ScheduleStatusActive,
time.Now(),
)
if err != nil {
return fmt.Errorf("failed to query due payments: %w", err)
}
defer rows.Close()
for rows.Next() {
var sp models.ScheduledPayment
err := rows.Scan(
&sp.ID,
&sp.CustomerID,
&sp.Amount,
&sp.Currency,
&sp.GatewayType,
&sp.PaymentMethodID,
&sp.ScheduleType,
&sp.Interval,
&sp.NextRunDate,
&sp.EndDate,
)
if err != nil {
continue
}
// Process payment
if err := s.processScheduledPayment(ctx, &sp); err != nil {
// Log error but continue processing other payments
fmt.Printf("Failed to process scheduled payment %s: %v\n", sp.ID, err)
}
}
return nil
}
// processScheduledPayment processes a single scheduled payment
func (s *SchedulerService) processScheduledPayment(
ctx context.Context,
scheduled *models.ScheduledPayment,
) error {
// Create payment request
paymentReq := &models.PaymentRequest{
Amount: scheduled.Amount,
Currency: scheduled.Currency,
GatewayType: scheduled.GatewayType,
CustomerID: scheduled.CustomerID,
PaymentMethodID: scheduled.PaymentMethodID,
Description: fmt.Sprintf("Scheduled payment: %s", scheduled.ID),
IdempotencyKey: fmt.Sprintf("%s_%s", scheduled.ID, time.Now().Format("20060102")),
}
// Process payment
transaction, err := s.transactionService.ProcessPayment(ctx, paymentReq)
if err != nil {
return fmt.Errorf("payment processing failed: %w", err)
}
// Update scheduled payment
nextRunDate := s.calculateNextRunDate(scheduled)
// Check if we should deactivate (reached end date)
status := models.ScheduleStatusActive
if scheduled.EndDate != nil && nextRunDate.After(*scheduled.EndDate) {
status = models.ScheduleStatusCompleted
}
query := `
UPDATE scheduled_payments
SET next_run_date = $1,
last_run_date = $2,
last_transaction_id = $3,
status = $4,
updated_at = $5
WHERE id = $6
`
_, err = s.db.ExecContext(ctx, query,
nextRunDate,
time.Now(),
transaction.ID,
status,
time.Now(),
scheduled.ID,
)
return err
}
// calculateNextRunDate calculates the next run date
func (s *SchedulerService) calculateNextRunDate(scheduled *models.ScheduledPayment) time.Time {
switch scheduled.ScheduleType {
case models.ScheduleTypeDaily:
return scheduled.NextRunDate.AddDate(0, 0, scheduled.Interval)
case models.ScheduleTypeWeekly:
return scheduled.NextRunDate.AddDate(0, 0, 7*scheduled.Interval)
case models.ScheduleTypeMonthly:
return scheduled.NextRunDate.AddDate(0, scheduled.Interval, 0)
case models.ScheduleTypeYearly:
return scheduled.NextRunDate.AddDate(scheduled.Interval, 0, 0)
default:
return scheduled.NextRunDate.AddDate(0, 0, 1)
}
}
// CancelScheduledPayment cancels a scheduled payment
func (s *SchedulerService) CancelScheduledPayment(ctx context.Context, id string) error {
query := `
UPDATE scheduled_payments
SET status = $1, updated_at = $2
WHERE id = $3
`
_, err := s.db.ExecContext(ctx, query,
models.ScheduleStatusCanceled,
time.Now(),
id,
)
return err
}
Models
// internal/models/payment.go
package models
import (
"time"
)
// Transaction represents a payment transaction
type Transaction struct {
ID string
Amount int64
Currency string
Status TransactionStatus
GatewayType string
GatewayTransactionID string
CustomerID string
PaymentMethodID string
IdempotencyKey string
Fee int64
NetAmount int64
ProcessedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
RetryCount int
MaxRetries int
ErrorMessage string
Metadata map[string]string
GatewayResponse map[string]interface{}
}
// TransactionStatus represents transaction status
type TransactionStatus string
const (
TransactionStatusPending TransactionStatus = "pending"
TransactionStatusProcessing TransactionStatus = "processing"
TransactionStatusSuccess TransactionStatus = "succeeded"
TransactionStatusFailed TransactionStatus = "failed"
TransactionStatusRefunded TransactionStatus = "refunded"
)
// PaymentRequest represents a payment request
type PaymentRequest struct {
Amount int64
Currency string
GatewayType string
CustomerID string
PaymentMethodID string
Description string
IdempotencyKey string
Metadata map[string]string
}
// ScheduledPayment represents a scheduled/recurring payment
type ScheduledPayment struct {
ID string
CustomerID string
Amount int64
Currency string
GatewayType string
PaymentMethodID string
ScheduleType ScheduleType
Interval int
StartDate time.Time
EndDate *time.Time
NextRunDate time.Time
LastRunDate *time.Time
LastTransactionID string
Status ScheduleStatus
CreatedAt time.Time
UpdatedAt time.Time
}
// ScheduleType represents schedule frequency
type ScheduleType string
const (
ScheduleTypeDaily ScheduleType = "daily"
ScheduleTypeWeekly ScheduleType = "weekly"
ScheduleTypeMonthly ScheduleType = "monthly"
ScheduleTypeYearly ScheduleType = "yearly"
)
// ScheduleStatus represents schedule status
type ScheduleStatus string
const (
ScheduleStatusActive ScheduleStatus = "active"
ScheduleStatusPaused ScheduleStatus = "paused"
ScheduleStatusCanceled ScheduleStatus = "canceled"
ScheduleStatusCompleted ScheduleStatus = "completed"
)
// ScheduledPaymentRequest represents a request to create a scheduled payment
type ScheduledPaymentRequest struct {
CustomerID string
Amount int64
Currency string
GatewayType string
PaymentMethodID string
ScheduleType ScheduleType
Interval int
StartDate time.Time
EndDate *time.Time
}
Database Schemas
SQL Schema (PostgreSQL)
// migrations/sql/001_create_tables.sql
package migrations
const SQLSchema = `
-- Transactions table
CREATE TABLE IF NOT EXISTS transactions (
id VARCHAR(255) PRIMARY KEY,
amount BIGINT NOT NULL,
currency VARCHAR(3) NOT NULL,
status VARCHAR(50) NOT NULL,
gateway_type VARCHAR(50) NOT NULL,
gateway_transaction_id VARCHAR(255),
customer_id VARCHAR(255) NOT NULL,
payment_method_id VARCHAR(255) NOT NULL,
idempotency_key VARCHAR(255) UNIQUE NOT NULL,
fee BIGINT DEFAULT 0,
net_amount BIGINT DEFAULT 0,
processed_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
retry_count INT DEFAULT 0,
max_retries INT DEFAULT 3,
error_message TEXT,
metadata JSONB,
gateway_response JSONB,
INDEX idx_transactions_customer_id (customer_id),
INDEX idx_transactions_status (status),
INDEX idx_transactions_created_at (created_at),
INDEX idx_transactions_idempotency_key (idempotency_key),
INDEX idx_transactions_gateway_type (gateway_type)
);
-- Scheduled payments table
CREATE TABLE IF NOT EXISTS scheduled_payments (
id VARCHAR(255) PRIMARY KEY,
customer_id VARCHAR(255) NOT NULL,
amount BIGINT NOT NULL,
currency VARCHAR(3) NOT NULL,
gateway_type VARCHAR(50) NOT NULL,
payment_method_id VARCHAR(255) NOT NULL,
schedule_type VARCHAR(50) NOT NULL,
interval_value INT NOT NULL DEFAULT 1,
start_date TIMESTAMP NOT NULL,
end_date TIMESTAMP,
next_run_date TIMESTAMP NOT NULL,
last_run_date TIMESTAMP,
last_transaction_id VARCHAR(255),
status VARCHAR(50) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
metadata JSONB,
INDEX idx_scheduled_customer_id (customer_id),
INDEX idx_scheduled_next_run_date (next_run_date),
INDEX idx_scheduled_status (status),
FOREIGN KEY (last_transaction_id) REFERENCES transactions(id)
);
-- Audit logs table
CREATE TABLE IF NOT EXISTS audit_logs (
id VARCHAR(255) PRIMARY KEY,
transaction_id VARCHAR(255) NOT NULL,
action VARCHAR(100) NOT NULL,
status VARCHAR(50) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
metadata JSONB,
ip_address VARCHAR(45),
user_agent TEXT,
INDEX idx_audit_transaction_id (transaction_id),
INDEX idx_audit_created_at (created_at),
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
);
-- Refunds table
CREATE TABLE IF NOT EXISTS refunds (
id VARCHAR(255) PRIMARY KEY,
transaction_id VARCHAR(255) NOT NULL,
amount BIGINT NOT NULL,
currency VARCHAR(3) NOT NULL,
status VARCHAR(50) NOT NULL,
gateway_refund_id VARCHAR(255),
reason VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP,
metadata JSONB,
INDEX idx_refunds_transaction_id (transaction_id),
INDEX idx_refunds_status (status),
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
);
-- Payment methods table
CREATE TABLE IF NOT EXISTS payment_methods (
id VARCHAR(255) PRIMARY KEY,
customer_id VARCHAR(255) NOT NULL,
gateway_type VARCHAR(50) NOT NULL,
gateway_payment_method_id VARCHAR(255) NOT NULL,
type VARCHAR(50) NOT NULL, -- card, bank_account, etc.
last_four VARCHAR(4),
brand VARCHAR(50),
exp_month INT,
exp_year INT,
is_default BOOLEAN DEFAULT FALSE,
status VARCHAR(50) NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_payment_methods_customer_id (customer_id),
INDEX idx_payment_methods_status (status)
);
-- Customers table
CREATE TABLE IF NOT EXISTS customers (
id VARCHAR(255) PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255),
phone VARCHAR(50),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
metadata JSONB,
INDEX idx_customers_email (email)
);
-- Idempotency keys table (for duplicate prevention)
CREATE TABLE IF NOT EXISTS idempotency_keys (
key VARCHAR(255) PRIMARY KEY,
transaction_id VARCHAR(255) NOT NULL,
response_data JSONB,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
INDEX idx_idempotency_expires_at (expires_at),
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
);
-- Create trigger to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_transactions_updated_at BEFORE UPDATE ON transactions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_scheduled_payments_updated_at BEFORE UPDATE ON scheduled_payments
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_payment_methods_updated_at BEFORE UPDATE ON payment_methods
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
`
NoSQL Schema (MongoDB)
// migrations/nosql/schema.go
package migrations
// MongoDB schema definitions
const MongoDBCollections = `
{
"transactions": {
"validator": {
"$jsonSchema": {
"bsonType": "object",
"required": ["_id", "amount", "currency", "status", "gateway_type", "customer_id", "created_at"],
"properties": {
"_id": {
"bsonType": "string",
"description": "Transaction ID"
},
"amount": {
"bsonType": "long",
"description": "Amount in smallest currency unit"
},
"currency": {
"bsonType": "string",
"pattern": "^[A-Z]{3}$",
"description": "ISO currency code"
},
"status": {
"enum": ["pending", "processing", "succeeded", "failed", "refunded"],
"description": "Transaction status"
},
"gateway_type": {
"bsonType": "string",
"description": "Payment gateway type"
},
"gateway_transaction_id": {
"bsonType": "string",
"description": "Gateway's transaction ID"
},
"customer_id": {
"bsonType": "string",
"description": "Customer identifier"
},
"payment_method_id": {
"bsonType": "string",
"description": "Payment method identifier"
},
"idempotency_key": {
"bsonType": "string",
"description": "Unique idempotency key"
},
"fee": {
"bsonType": "long",
"description": "Transaction fee"
},
"net_amount": {
"bsonType": "long",
"description": "Net amount after fees"
},
"processed_at": {
"bsonType": "date",
"description": "Processing timestamp"
},
"created_at": {
"bsonType": "date",
"description": "Creation timestamp"
},
"updated_at": {
"bsonType": "date",
"description": "Last update timestamp"
},
"retry_count": {
"bsonType": "int",
"minimum": 0,
"description": "Number of retry attempts"
},
"max_retries": {
"bsonType": "int",
"minimum": 0,
"description": "Maximum retry attempts"
},
"error_message": {
"bsonType": "string",
"description": "Error message if failed"
},
"metadata": {
"bsonType": "object",
"description": "Additional metadata"
},
"gateway_response": {
"bsonType": "object",
"description": "Full gateway response"
}
}
}
},
"indexes": [
{"key": {"customer_id": 1}, "name": "idx_customer_id"},
{"key": {"status": 1}, "name": "idx_status"},
{"key": {"created_at": -1}, "name": "idx_created_at"},
{"key": {"idempotency_key": 1}, "unique": true, "name": "idx_idempotency_key"},
{"key": {"gateway_type": 1}, "name": "idx_gateway_type"},
{"key": {"customer_id": 1, "status": 1, "created_at": -1}, "name": "idx_customer_status_created"}
]
},
"scheduled_payments": {
"validator": {
"$jsonSchema": {
"bsonType": "object",
"required": ["_id", "customer_id", "amount", "currency", "schedule_type", "next_run_date", "status"],
"properties": {
"_id": {"bsonType": "string"},
"customer_id": {"bsonType": "string"},
"amount": {"bsonType": "long"},
"currency": {"bsonType": "string", "pattern": "^[A-Z]{3}$"},
"gateway_type": {"bsonType": "string"},
"payment_method_id": {"bsonType": "string"},
"schedule_type": {
"enum": ["daily", "weekly", "monthly", "yearly"]
},
"interval": {"bsonType": "int", "minimum": 1},
"start_date": {"bsonType": "date"},
"end_date": {"bsonType": "date"},
"next_run_date": {"bsonType": "date"},
"last_run_date": {"bsonType": "date"},
"last_transaction_id": {"bsonType": "string"},
"status": {
"enum": ["active", "paused", "canceled", "completed"]
},
"created_at": {"bsonType": "date"},
"updated_at": {"bsonType": "date"}
}
}
},
"indexes": [
{"key": {"customer_id": 1}, "name": "idx_customer_id"},
{"key": {"next_run_date": 1, "status": 1}, "name": "idx_next_run_status"},
{"key": {"status": 1}, "name": "idx_status"}
]
},
"audit_logs": {
"indexes": [
{"key": {"transaction_id": 1}, "name": "idx_transaction_id"},
{"key": {"created_at": -1}, "name": "idx_created_at"},
{"key": {"transaction_id": 1, "created_at": -1}, "name": "idx_transaction_created"}
],
"timeseries": {
"timeField": "created_at",
"metaField": "transaction_id",
"granularity": "seconds"
}
}
}
`
// Sample MongoDB document structures
type TransactionDocument struct {
ID string `bson:"_id"`
Amount int64 `bson:"amount"`
Currency string `bson:"currency"`
Status string `bson:"status"`
GatewayType string `bson:"gateway_type"`
GatewayTransactionID string `bson:"gateway_transaction_id,omitempty"`
CustomerID string `bson:"customer_id"`
PaymentMethodID string `bson:"payment_method_id"`
IdempotencyKey string `bson:"idempotency_key"`
Fee int64 `bson:"fee"`
NetAmount int64 `bson:"net_amount"`
ProcessedAt *time.Time `bson:"processed_at,omitempty"`
CreatedAt time.Time `bson:"created_at"`
UpdatedAt time.Time `bson:"updated_at"`
RetryCount int `bson:"retry_count"`
MaxRetries int `bson:"max_retries"`
ErrorMessage string `bson:"error_message,omitempty"`
Metadata map[string]interface{} `bson:"metadata,omitempty"`
GatewayResponse map[string]interface{} `bson:"gateway_response,omitempty"`
}
Main Application
// cmd/api/main.go
package main
import (
"context"
"database/sql"
"encoding/json"
"log"
"net/http"
"time"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
"payment-gateway/internal/gateway"
"payment-gateway/internal/models"
"payment-gateway/internal/service"
)
func main() {
// Initialize database
db, err := sql.Open("postgres",
"postgres://user:password@localhost/payments?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Initialize gateway factory
gatewayFactory := gateway.NewGatewayFactory()
// Register Stripe gateway
stripeGateway := gateway.NewStripeGateway(
"sk_test_...",
"whsec_...",
)
gatewayFactory.RegisterGateway(gateway.GatewayTypeStripe, stripeGateway)
// Register PayPal gateway
paypalGateway := gateway.NewPayPalGateway(
"client_id",
"client_secret",
true, // sandbox
)
gatewayFactory.RegisterGateway(gateway.GatewayTypePayPal, paypalGateway)
// Initialize services
retryService := service.NewRetryService()
transactionService := service.NewTransactionService(db, gatewayFactory, retryService)
schedulerService := service.NewSchedulerService(db, transactionService)
// Start scheduler in background
go func() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for range ticker.C {
if err := schedulerService.ProcessDuePayments(context.Background()); err != nil {
log.Printf("Error processing scheduled payments: %v", err)
}
}
}()
// Setup router
router := mux.NewRouter()
// Process payment endpoint
router.HandleFunc("/api/v1/payments", func(w http.ResponseWriter, r *http.Request) {
var req models.PaymentRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
transaction, err := transactionService.ProcessPayment(r.Context(), &req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(transaction)
}).Methods("POST")
// Create scheduled payment endpoint
router.HandleFunc("/api/v1/scheduled-payments", func(w http.ResponseWriter, r *http.Request) {
var req models.ScheduledPaymentRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
scheduled, err := schedulerService.CreateScheduledPayment(r.Context(), &req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(scheduled)
}).Methods("POST")
// Health check
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
})
log.Println("Payment gateway server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", router))
}
Best Practices
1. Idempotency
Always implement idempotency to prevent duplicate charges:
func (s *TransactionService) CheckIdempotency(
ctx context.Context,
key string,
) (*models.Transaction, bool, error) {
query := `
SELECT transaction_id, response_data
FROM idempotency_keys
WHERE key = $1 AND expires_at > NOW()
`
var transactionID string
var responseData []byte
err := s.db.QueryRowContext(ctx, query, key).Scan(&transactionID, &responseData)
if err == sql.ErrNoRows {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
// Return cached transaction
var transaction models.Transaction
if err := json.Unmarshal(responseData, &transaction); err != nil {
return nil, false, err
}
return &transaction, true, nil
}
2. PCI Compliance
Never store sensitive card data:
// DON'T store raw card numbers
type PaymentMethod struct {
CardNumber string // ❌ Never do this
CVV string // ❌ Never do this
}
// DO store gateway tokens
type PaymentMethod struct {
GatewayPaymentMethodID string // ✅ Gateway token
LastFour string // ✅ Safe to store
Brand string // ✅ Safe to store
}
3. Atomic Operations
Use database transactions for atomicity:
func (s *TransactionService) ProcessWithRefund(
ctx context.Context,
originalTxnID string,
refundAmount int64,
) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// Update original transaction
_, err = tx.ExecContext(ctx,
"UPDATE transactions SET status = $1 WHERE id = $2",
models.TransactionStatusRefunded,
originalTxnID,
)
if err != nil {
return err
}
// Create refund record
_, err = tx.ExecContext(ctx,
"INSERT INTO refunds (id, transaction_id, amount, status) VALUES ($1, $2, $3, $4)",
generateID(),
originalTxnID,
refundAmount,
"succeeded",
)
if err != nil {
return err
}
return tx.Commit()
}
4. Webhook Handling
Always verify webhook signatures:
func (h *WebhookHandler) HandleStripeWebhook(w http.ResponseWriter, r *http.Request) {
payload, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request", http.StatusBadRequest)
return
}
signature := r.Header.Get("Stripe-Signature")
// Verify webhook
if err := h.gateway.VerifyWebhook(payload, signature); err != nil {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process webhook
// ...
w.WriteHeader(http.StatusOK)
}
Comparison: SQL vs NoSQL for Payment Data
When to Use SQL
Advantages:
- Strong ACID guarantees
- Complex queries and joins
- Transaction integrity
- Mature tooling
Best for:
- Financial transactions requiring strict consistency
- Complex reporting and analytics
- Refunds and chargebacks
- Audit trails
When to Use NoSQL
Advantages:
- High write throughput
- Flexible schema
- Horizontal scaling
- Better for time-series data
Best for:
- Payment event logs
- Session data
- Caching payment methods
- Analytics and metrics
Hybrid Approach
Many production systems use both:
type HybridPaymentRepo struct {
sql *sql.DB // For transactions
mongo *mongo.Client // For logs and analytics
}
func (r *HybridPaymentRepo) ProcessPayment(
ctx context.Context,
payment *models.Payment,
) error {
// Store transaction in SQL for ACID guarantees
if err := r.saveTransactionSQL(ctx, payment); err != nil {
return err
}
// Store event log in MongoDB for analytics
go r.logEventMongo(ctx, payment) // Async
return nil
}
Monitoring and Observability
type PaymentMetrics struct {
TotalTransactions int64
SuccessfulPayments int64
FailedPayments int64
TotalRevenue int64
AverageProcessTime time.Duration
RetryRate float64
}
func (s *TransactionService) GetMetrics(
ctx context.Context,
start, end time.Time,
) (*PaymentMetrics, error) {
query := `
SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'succeeded' THEN 1 ELSE 0 END) as successful,
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed,
SUM(CASE WHEN status = 'succeeded' THEN amount ELSE 0 END) as revenue,
AVG(retry_count) as avg_retries
FROM transactions
WHERE created_at BETWEEN $1 AND $2
`
var metrics PaymentMetrics
var avgRetries float64
err := s.db.QueryRowContext(ctx, query, start, end).Scan(
&metrics.TotalTransactions,
&metrics.SuccessfulPayments,
&metrics.FailedPayments,
&metrics.TotalRevenue,
&avgRetries,
)
metrics.RetryRate = avgRetries / float64(metrics.TotalTransactions)
return &metrics, err
}
Conclusion
Building a robust payment gateway integration requires careful consideration of:
- Multiple Gateway Support: Use the Strategy pattern for flexibility
- Transaction Safety: Implement proper atomic operations and rollback
- Retry Logic: Use exponential backoff for transient failures
- Idempotency: Prevent duplicate charges
- Scheduling: Support recurring and delayed payments
- Data Persistence: Choose the right database for your needs
- Security: Follow PCI compliance guidelines
- Monitoring: Track metrics and failures
The examples in this guide provide a solid foundation for building production-ready payment systems in Go.
Go Architecture Patterns Series: ← Previous: Saga Pattern | Series Overview