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.

graph TB subgraph "Client Layer" API[Payment API] end subgraph "Service Layer" PM[Payment Manager] TS[Transaction Service] SS[Scheduler Service] RS[Retry Service] end subgraph "Gateway Abstraction" PG[Payment Gateway Interface] end subgraph "Gateway Implementations" Stripe[Stripe Gateway] PayPal[PayPal Gateway] Square[Square Gateway] Custom[Custom Gateway] end subgraph "Storage Layer" SQL[(SQL Database)] NoSQL[(NoSQL Database)] Cache[(Redis Cache)] end API --> PM PM --> TS PM --> SS TS --> RS PM --> PG PG --> Stripe PG --> PayPal PG --> Square PG --> Custom TS --> SQL TS --> NoSQL SS --> Cache style PM fill:#fef3c7 style PG fill:#dbeafe style SQL fill:#d1fae5 style NoSQL fill:#fce7f3

Transaction Flow

Understanding the complete payment transaction flow is crucial for building reliable systems.

sequenceDiagram participant Client participant PaymentAPI participant TransactionService participant Gateway participant Database participant AuditLog Note over Client,AuditLog: Successful Payment Flow Client->>PaymentAPI: InitiatePayment(request) PaymentAPI->>TransactionService: CreateTransaction() TransactionService->>Database: BeginTransaction() Database-->>TransactionService: TX Started TransactionService->>Database: SaveTransaction(PENDING) Database-->>TransactionService: Saved TransactionService->>Gateway: ProcessPayment(details) alt Payment Successful Gateway-->>TransactionService: Success(transactionID) TransactionService->>Database: UpdateTransaction(SUCCESS) TransactionService->>AuditLog: LogSuccess() TransactionService->>Database: CommitTransaction() Database-->>TransactionService: Committed TransactionService-->>PaymentAPI: PaymentSuccess PaymentAPI-->>Client: 200 OK else Payment Failed Gateway-->>TransactionService: Failure(error) TransactionService->>Database: UpdateTransaction(FAILED) TransactionService->>AuditLog: LogFailure() TransactionService->>Database: CommitTransaction() TransactionService-->>PaymentAPI: PaymentFailed PaymentAPI-->>Client: 402 Payment Required else Network Error Gateway-->>TransactionService: Timeout/Error TransactionService->>Database: UpdateTransaction(RETRY_PENDING) TransactionService->>Database: CommitTransaction() Note over TransactionService: Queue for retry TransactionService-->>PaymentAPI: PaymentPending PaymentAPI-->>Client: 202 Accepted end

Retry Mechanism Flow

stateDiagram-v2 [*] --> Pending Pending --> Processing: InitiatePayment Processing --> Success: GatewaySuccess Processing --> Failed: GatewayRejected Processing --> RetryPending: NetworkError/Timeout RetryPending --> Retry1: ExponentialBackoff(2s) Retry1 --> Success: GatewaySuccess Retry1 --> Failed: GatewayRejected Retry1 --> Retry2: NetworkError/Timeout Retry2 --> Success: GatewaySuccess Retry2 --> Failed: GatewayRejected Retry2 --> Retry3: NetworkError/Timeout Retry3 --> Success: GatewaySuccess Retry3 --> Failed: GatewayRejected Retry3 --> MaxRetriesReached: NetworkError/Timeout MaxRetriesReached --> ManualReview ManualReview --> Success: AdminRetry ManualReview --> Failed: AdminCancel Success --> [*] Failed --> [*] note right of RetryPending Retry with exponential backoff: - Retry 1: 2s - Retry 2: 4s - Retry 3: 8s end note

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:

  1. Multiple Gateway Support: Use the Strategy pattern for flexibility
  2. Transaction Safety: Implement proper atomic operations and rollback
  3. Retry Logic: Use exponential backoff for transient failures
  4. Idempotency: Prevent duplicate charges
  5. Scheduling: Support recurring and delayed payments
  6. Data Persistence: Choose the right database for your needs
  7. Security: Follow PCI compliance guidelines
  8. 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