Please hold on for a moment while the content loads.
Please hold on for a moment while the content loads.
Unified Multi-Gateway Payment Platform
A centralized payment service for checkout, payment tracking, order management, and refund operations across multiple gateways.
A centralized payment service that gives education products one checkout and payment-management surface across Stripe, bKash, and SSLCommerz. The service exposes OpenAPI-defined Go endpoints for payments, orders, products, users, organizations, and refunds, backed by PostgreSQL with Redis integration and a Nuxt administration dashboard.
System architecture showing adapter pattern with three payment gateway integrations
Gateway callback flow validating and persisting payment status
Unified interface for Stripe, bKash, and SSLCommerz. New gateways can be added by implementing the adapter interface.
Gateway-specific checkout and callback code is kept separate from payment persistence, allowing provider behavior to evolve without spreading gateway details through the dashboard and API layers.
Gateway callbacks are validated and mapped back to local payment records so checkout results update the service's payment status.
Payment records track pending, completed, failed, refunded, and cancelled outcomes together with gateway responses and failure reasons.
PostgreSQL stores payment and order records, while Redis supports low-latency gateway token access and ClickHouse is available for analytics workloads.
Refund records can be created, listed, inspected, updated, and tracked alongside their related payments.
Different payment providers have different APIs, authentication methods, and webhook formats. The system needs to support multiple providers without coupling business logic to any single provider.
Each gateway implements a common adapter interface. Core business logic operates on the interface, not concrete implementations. Adding a new gateway means writing one new adapter.
// PaymentGateway: business logic depends on this interface, never on concrete gateways.
type PaymentGateway interface {
CreatePayment(ctx context.Context, req PaymentRequest) (*PaymentResult, error)
VerifyWebhook(payload []byte, signature string) (WebhookEvent, error)
Refund(ctx context.Context, id string, amount int64) (*RefundResult, error)
}
// StripeAdapter implements PaymentGateway for Stripe.
type StripeAdapter struct {
client *stripe.Client
}
func (a *StripeAdapter) CreatePayment(ctx context.Context, req PaymentRequest) (*PaymentResult, error) {
pi, err := a.client.PaymentIntents.New(&stripe.PaymentIntentParams{
Amount: stripe.Int64(req.Amount),
Currency: stripe.String(req.Currency),
})
if err != nil {
return nil, fmt.Errorf("stripe: %w", err)
}
return &PaymentResult{
ExternalID: pi.ID,
Status: mapStripeStatus(pi.Status),
}, nil
}Payment APIs have strict contract requirements (idempotency keys, request signing, response validation). Hand-written API code is error-prone.
API specification is defined in OpenAPI format. Server and client code are generated from the spec, ensuring request/response contract compliance.
Transaction data needs ACID guarantees, analytics queries need aggregation performance, and gateway tokens need low-latency access.
PostgreSQL for transaction records (ACID), a columnar database for analytics (aggregations), and Redis for gateway token caching (low latency).
Payments transition through defined states (pending, completed, failed, refunded). Without guards, invalid transitions can corrupt payment records.
A state machine governs all transitions with guard functions preventing invalid changes. Each transition is logged for audit trail.
// Payment state machine: guards prevent invalid transitions.
var transitions = map[PaymentState][]PaymentState{
StatePending: {StateCompleted, StateFailed},
StateCompleted: {StateRefunded},
StateFailed: {},
StateRefunded: {},
}
func TransitionPayment(p *Payment, target PaymentState) error {
for _, s := range transitions[p.Status] {
if s == target {
p.Status = target
p.UpdatedAt = time.Now()
return nil
}
}
return fmt.Errorf("invalid transition: %s -> %s", p.Status, target)
}Payment gateways can deliver duplicate webhooks. Processing the same event twice (e.g., charging a user twice) must be prevented.
Idempotency keys with Redis-backed deduplication (24-hour TTL) ensure each event is processed exactly once, with automatic retry for transient failures.
// WebhookHandler processes gateway callbacks with idempotency protection.
func (h *WebhookHandler) Handle(w http.ResponseWriter, r *http.Request) {
event, err := h.gateway.VerifyWebhook(r.Body, r.Header.Get("X-Signature"))
if err != nil {
http.Error(w, "invalid signature", http.StatusBadRequest)
return
}
// Idempotency check: skip if already processed
key := "webhook:" + event.ID
if exists, _ := h.redis.Exists(r.Context(), key).Result(); exists == 1 {
w.WriteHeader(http.StatusOK)
return
}
if err := h.processEvent(r.Context(), event); err != nil {
http.Error(w, "processing failed", http.StatusInternalServerError)
return
}
// Mark as processed (TTL matches provider retry window)
h.redis.Set(r.Context(), key, "1", 24*time.Hour)
w.WriteHeader(http.StatusOK)
}Loose coupling, easy to add new gateways, centralized error handling
Abstraction overhead, loss of gateway-specific optimizations, interface design complexity
Contract enforcement, type safety, reduced boilerplate
Generated code can be verbose, spec drift risk, build-time dependency
Each store optimized for its workload pattern
Operational complexity, eventual consistency, more infrastructure to manage
Gateway Timeout / Unreachable
Payment processing fails, user sees error
Retry with exponential backoff for transient failures. Circuit breaker pattern prevents cascading retries to unhealthy gateways. Alert on persistent gateway unavailability.
Webhook Signature Mismatch
Legitimate payment event not processed
Failed signature verification logs full payload for manual review. Automatic retry from gateway (most gateways retry webhooks for 24-72 hours). Dashboard for manual reconciliation.
Duplicate Webhook Delivery
Same event processed twice, potential double charge
Idempotency keys with idempotency store in Redis. Duplicate events return cached response. State machine prevents invalid transitions (e.g., completing an already-completed payment).
Database Transaction Failure
Payment state inconsistent between gateway and local storage
Write-ahead logging for payment events. Reconciliation job periodically compares local state with gateway state. Manual override API for edge cases.
Refund Race Condition
Refund initiated twice for same transaction
Refund idempotency via refund IDempotency key. State machine guards prevent double refund. Gateway-side deduplication as last resort.
Individual adapters, state machine transitions, webhook signature verification
Vitest with mocked gateway HTTP clients. Tests cover: adapter interface compliance, invalid state transitions, malformed webhook payloads, signature verification edge cases.
API endpoints with real database, webhook processing pipeline
Testcontainers for PostgreSQL and Redis in CI. Tests verify: payment lifecycle through all states, webhook delivery with real signature verification, idempotency across duplicate events.
Full payment flow: initiate → gateway redirect → webhook → completion
Gateway sandbox environments for Stripe, bKash, and SSLCommerz. Tests cover: successful payment, failed payment, refund flow, dispute handling.
The adapter pattern makes adding a new payment gateway a contained task: implement the interface, write tests, and deploy.
Webhook handlers must be idempotent: duplicate events happen and processing them twice means charging customers twice.
Transactions need ACID, analytics need columnar storage, and gateway tokens need caching. Using one database for all would compromise on everything.
A payment state machine prevents invalid transitions like refunding an already-refunded payment or completing a failed transaction.
A deep dive into the adapter pattern used for this project. abstracting multiple payment gateways behind a unified interface.
Read the full gist →This is a proprietary production system. The details above reflect my actual work. Additional evidence available upon request:
Contact me to arrange verification under NDA.