Payment Service - Database Design
Database Architecture Overview
Multi-Database Strategy
┌─────────────────────────────────────────────────────────────────┐
│ Application Services │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ Data Access Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐│
│ │ Transaction │ │ Cache │ │ Search │ │ Stream ││
│ │ ORM │ │ Client │ │ Client │ │ Client ││
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘│
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ Database Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ PostgreSQL │ │ Redis │ │Elasticsearch│ │ Kafka │ │
│ │(ACID Trans) │ │ (Cache) │ │(Analytics) │ │ (Events) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘Core Transaction Schema (PostgreSQL)
Transactions Table
-- Main transactions table with ACID compliance
CREATE TABLE transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
merchant_id UUID NOT NULL,
customer_id UUID,
-- Financial data
amount BIGINT NOT NULL CHECK (amount > 0), -- Amount in smallest currency unit
currency VARCHAR(3) NOT NULL,
fee_amount BIGINT DEFAULT 0,
net_amount BIGINT GENERATED ALWAYS AS (amount - fee_amount) STORED,
-- Transaction metadata
status VARCHAR(20) NOT NULL DEFAULT 'pending',
payment_method_type VARCHAR(20) NOT NULL,
payment_method_token VARCHAR(100),
-- Gateway information
gateway_name VARCHAR(50) NOT NULL,
gateway_transaction_id VARCHAR(100),
gateway_response JSONB,
-- Risk and compliance
fraud_score DECIMAL(5,4),
risk_level VARCHAR(10),
compliance_status VARCHAR(20) DEFAULT 'pending',
-- Audit trail
description TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Constraints
CONSTRAINT valid_status CHECK (
status IN ('pending', 'authorized', 'captured', 'succeeded',
'failed', 'cancelled', 'refunded', 'disputed')
),
CONSTRAINT valid_risk_level CHECK (
risk_level IN ('low', 'medium', 'high') OR risk_level IS NULL
)
);
-- Performance indexes
CREATE INDEX idx_transactions_merchant_created ON transactions(merchant_id, created_at DESC);
CREATE INDEX idx_transactions_customer ON transactions(customer_id) WHERE customer_id IS NOT NULL;
CREATE INDEX idx_transactions_status ON transactions(status) WHERE status IN ('pending', 'failed');
CREATE INDEX idx_transactions_gateway ON transactions(gateway_name, gateway_transaction_id);
CREATE INDEX idx_transactions_amount_currency ON transactions(currency, amount);
-- Partial index for high-risk transactions
CREATE INDEX idx_transactions_high_risk ON transactions(created_at DESC)
WHERE risk_level = 'high' OR fraud_score > 0.8;Payment Methods Vault
-- Secure storage for tokenized payment methods
CREATE TABLE payment_methods_vault (
token VARCHAR(100) PRIMARY KEY,
customer_id UUID NOT NULL,
merchant_id UUID NOT NULL,
-- Encrypted payment data
encrypted_data BYTEA NOT NULL,
encryption_key_id VARCHAR(100) NOT NULL,
-- Metadata for identification
payment_method_type VARCHAR(20) NOT NULL,
fingerprint VARCHAR(64) NOT NULL,
last_four VARCHAR(4),
brand VARCHAR(20), -- Visa, Mastercard, etc.
-- Expiration and lifecycle
expires_at DATE,
is_active BOOLEAN DEFAULT true,
-- Audit
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_used_at TIMESTAMP WITH TIME ZONE,
-- Unique constraint to prevent duplicates
UNIQUE(customer_id, fingerprint)
);
-- Indexes for payment methods
CREATE INDEX idx_payment_methods_customer ON payment_methods_vault(customer_id, is_active);
CREATE INDEX idx_payment_methods_merchant ON payment_methods_vault(merchant_id);
CREATE INDEX idx_payment_methods_expires ON payment_methods_vault(expires_at)
WHERE expires_at IS NOT NULL AND is_active = true;Refunds and Disputes
-- Refunds table
CREATE TABLE refunds (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
transaction_id UUID NOT NULL REFERENCES transactions(id),
-- Refund details
amount BIGINT NOT NULL CHECK (amount > 0),
currency VARCHAR(3) NOT NULL,
reason VARCHAR(100),
-- Processing information
status VARCHAR(20) NOT NULL DEFAULT 'pending',
gateway_refund_id VARCHAR(100),
-- Audit
requested_by UUID, -- User who requested refund
processed_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT valid_refund_status CHECK (
status IN ('pending', 'succeeded', 'failed', 'cancelled')
)
);
-- Disputes table
CREATE TABLE disputes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
transaction_id UUID NOT NULL REFERENCES transactions(id),
-- Dispute details
dispute_type VARCHAR(50) NOT NULL, -- chargeback, inquiry, etc.
reason_code VARCHAR(20),
amount BIGINT NOT NULL,
currency VARCHAR(3) NOT NULL,
-- Status and timeline
status VARCHAR(20) NOT NULL DEFAULT 'open',
due_date DATE,
-- Evidence and documentation
evidence JSONB DEFAULT '{}',
merchant_response TEXT,
-- Gateway information
gateway_dispute_id VARCHAR(100),
-- Audit
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
resolved_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT valid_dispute_status CHECK (
status IN ('open', 'under_review', 'won', 'lost', 'warning_closed')
)
);Merchant and Customer Schema
Merchants Table
-- Merchant accounts
CREATE TABLE merchants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Business information
business_name VARCHAR(200) NOT NULL,
legal_name VARCHAR(200),
business_type VARCHAR(50), -- corporation, llc, sole_proprietorship
tax_id VARCHAR(50),
-- Contact information
email VARCHAR(255) NOT NULL UNIQUE,
phone VARCHAR(20),
website VARCHAR(255),
-- Address
address JSONB NOT NULL,
-- Account status
status VARCHAR(20) NOT NULL DEFAULT 'pending',
verification_status VARCHAR(20) DEFAULT 'unverified',
-- Processing configuration
processing_config JSONB DEFAULT '{}',
fee_structure JSONB DEFAULT '{}',
-- Risk and limits
risk_level VARCHAR(10) DEFAULT 'medium',
daily_limit BIGINT,
monthly_limit BIGINT,
-- Audit
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT valid_merchant_status CHECK (
status IN ('pending', 'active', 'suspended', 'closed')
)
);
-- API keys for merchant authentication
CREATE TABLE merchant_api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
merchant_id UUID NOT NULL REFERENCES merchants(id) ON DELETE CASCADE,
-- Key information
key_hash VARCHAR(128) NOT NULL UNIQUE, -- Hashed API key
key_prefix VARCHAR(20) NOT NULL, -- First few chars for identification
-- Permissions and scope
permissions TEXT[] DEFAULT '{}',
environment VARCHAR(20) NOT NULL DEFAULT 'live', -- live, test
-- Status and lifecycle
is_active BOOLEAN DEFAULT true,
expires_at TIMESTAMP WITH TIME ZONE,
last_used_at TIMESTAMP WITH TIME ZONE,
-- Audit
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_by UUID,
CONSTRAINT valid_environment CHECK (environment IN ('live', 'test'))
);Customers Table
-- Customer records
CREATE TABLE customers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
merchant_id UUID NOT NULL REFERENCES merchants(id),
-- Customer identification
external_customer_id VARCHAR(100), -- Merchant's customer ID
email VARCHAR(255),
phone VARCHAR(20),
-- Personal information (encrypted)
encrypted_pii BYTEA,
encryption_key_id VARCHAR(100),
-- Risk assessment
risk_score DECIMAL(5,4) DEFAULT 0.0,
risk_level VARCHAR(10) DEFAULT 'low',
-- Behavioral data
total_transactions INTEGER DEFAULT 0,
total_amount BIGINT DEFAULT 0,
first_transaction_at TIMESTAMP WITH TIME ZONE,
last_transaction_at TIMESTAMP WITH TIME ZONE,
-- Status
is_active BOOLEAN DEFAULT true,
-- Audit
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Unique constraint per merchant
UNIQUE(merchant_id, external_customer_id)
);Analytics and Reporting Schema
Transaction Analytics
-- Daily transaction summaries for reporting
CREATE TABLE daily_transaction_summaries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
merchant_id UUID NOT NULL REFERENCES merchants(id),
-- Date and currency
summary_date DATE NOT NULL,
currency VARCHAR(3) NOT NULL,
-- Volume metrics
transaction_count INTEGER NOT NULL DEFAULT 0,
successful_count INTEGER NOT NULL DEFAULT 0,
failed_count INTEGER NOT NULL DEFAULT 0,
refunded_count INTEGER NOT NULL DEFAULT 0,
-- Amount metrics
gross_amount BIGINT NOT NULL DEFAULT 0,
fee_amount BIGINT NOT NULL DEFAULT 0,
net_amount BIGINT NOT NULL DEFAULT 0,
refunded_amount BIGINT NOT NULL DEFAULT 0,
-- Performance metrics
success_rate DECIMAL(5,4) GENERATED ALWAYS AS (
CASE WHEN transaction_count > 0
THEN successful_count::DECIMAL / transaction_count
ELSE 0 END
) STORED,
-- Risk metrics
high_risk_count INTEGER DEFAULT 0,
fraud_count INTEGER DEFAULT 0,
dispute_count INTEGER DEFAULT 0,
-- Timestamps
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Unique constraint
UNIQUE(merchant_id, summary_date, currency)
);
-- Indexes for analytics queries
CREATE INDEX idx_daily_summaries_merchant_date ON daily_transaction_summaries(merchant_id, summary_date DESC);
CREATE INDEX idx_daily_summaries_date ON daily_transaction_summaries(summary_date DESC);Caching Strategy (Redis)
Cache Key Patterns
# Redis cache key patterns and TTL strategy
class PaymentCacheKeys:
# Merchant data (1 hour TTL)
MERCHANT_CONFIG = "merchant:config:{merchant_id}"
MERCHANT_LIMITS = "merchant:limits:{merchant_id}"
# Payment method tokens (24 hours TTL)
PAYMENT_METHOD = "pm:token:{token}"
CUSTOMER_PAYMENT_METHODS = "customer:pm:{customer_id}"
# Fraud detection (15 minutes TTL)
FRAUD_SCORE = "fraud:score:{transaction_id}"
VELOCITY_CHECK = "velocity:{customer_id}:{timeframe}"
# Gateway routing (30 minutes TTL)
GATEWAY_HEALTH = "gateway:health:{gateway_name}"
ROUTING_RULES = "routing:rules:{merchant_id}"
# Rate limiting (1 hour sliding window)
RATE_LIMIT = "rate_limit:{api_key}:{endpoint}:{window}"
class PaymentCache:
def __init__(self, redis_client):
self.redis = redis_client
self.default_ttl = 3600 # 1 hour
async def cache_merchant_config(self, merchant_id: str, config: dict):
"""Cache merchant configuration"""
key = PaymentCacheKeys.MERCHANT_CONFIG.format(merchant_id=merchant_id)
await self.redis.setex(key, 3600, json.dumps(config))
async def get_merchant_config(self, merchant_id: str) -> Optional[dict]:
"""Retrieve cached merchant configuration"""
key = PaymentCacheKeys.MERCHANT_CONFIG.format(merchant_id=merchant_id)
cached_data = await self.redis.get(key)
return json.loads(cached_data) if cached_data else None
async def cache_payment_method(self, token: str, payment_method: dict):
"""Cache decrypted payment method data"""
key = PaymentCacheKeys.PAYMENT_METHOD.format(token=token)
await self.redis.setex(key, 86400, json.dumps(payment_method)) # 24 hours
async def increment_velocity_counter(
self,
customer_id: str,
timeframe: str,
amount: int
):
"""Increment velocity counters for fraud detection"""
count_key = f"velocity:{customer_id}:{timeframe}:count"
amount_key = f"velocity:{customer_id}:{timeframe}:amount"
pipe = self.redis.pipeline()
pipe.incr(count_key)
pipe.incrbyfloat(amount_key, amount)
# Set expiration based on timeframe
ttl = {'hour': 3600, 'day': 86400, 'week': 604800}.get(timeframe, 3600)
pipe.expire(count_key, ttl)
pipe.expire(amount_key, ttl)
await pipe.execute()Database Optimization
Partitioning Strategy
-- Partition transactions table by month for better performance
CREATE TABLE transactions_2024_01 PARTITION OF transactions
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE transactions_2024_02 PARTITION OF transactions
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
-- Automated partition creation function
CREATE OR REPLACE FUNCTION create_monthly_partition(table_name TEXT, start_date DATE)
RETURNS VOID AS $$
DECLARE
partition_name TEXT;
end_date DATE;
BEGIN
partition_name := table_name || '_' || to_char(start_date, 'YYYY_MM');
end_date := start_date + INTERVAL '1 month';
EXECUTE format('CREATE TABLE %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)',
partition_name, table_name, start_date, end_date);
-- Create indexes on partition
EXECUTE format('CREATE INDEX %I ON %I (merchant_id, created_at DESC)',
'idx_' || partition_name || '_merchant_created', partition_name);
END;
$$ LANGUAGE plpgsql;Query Optimization
-- Materialized view for merchant analytics
CREATE MATERIALIZED VIEW merchant_monthly_stats AS
SELECT
merchant_id,
DATE_TRUNC('month', created_at) as month,
currency,
COUNT(*) as transaction_count,
COUNT(*) FILTER (WHERE status = 'succeeded') as successful_count,
SUM(amount) FILTER (WHERE status = 'succeeded') as total_amount,
AVG(amount) FILTER (WHERE status = 'succeeded') as avg_amount,
COUNT(*) FILTER (WHERE risk_level = 'high') as high_risk_count
FROM transactions
WHERE created_at >= CURRENT_DATE - INTERVAL '2 years'
GROUP BY merchant_id, DATE_TRUNC('month', created_at), currency;
-- Refresh materialized view daily
CREATE INDEX idx_merchant_monthly_stats ON merchant_monthly_stats(merchant_id, month DESC);
-- Automated refresh job
SELECT cron.schedule('refresh-merchant-stats', '0 2 * * *',
'REFRESH MATERIALIZED VIEW CONCURRENTLY merchant_monthly_stats;');Data Consistency and ACID
Transaction Isolation
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from contextlib import asynccontextmanager
class PaymentTransactionManager:
def __init__(self, connection_string: str):
self.engine = create_engine(connection_string)
self.Session = sessionmaker(bind=self.engine)
@asynccontextmanager
async def payment_transaction(self, isolation_level='READ_COMMITTED'):
"""Context manager for payment transactions with proper isolation"""
session = self.Session()
try:
# Set isolation level
await session.execute(text(f"SET TRANSACTION ISOLATION LEVEL {isolation_level}"))
yield session
await session.commit()
except Exception as e:
await session.rollback()
raise PaymentTransactionException(f"Transaction failed: {e}")
finally:
session.close()
async def process_payment_with_refund_protection(
self,
transaction_data: dict,
refund_data: Optional[dict] = None
):
"""Process payment with atomic refund handling"""
async with self.payment_transaction('SERIALIZABLE') as session:
# Create main transaction
transaction = await session.execute(
text("""
INSERT INTO transactions
(merchant_id, amount, currency, status, payment_method_token)
VALUES (:merchant_id, :amount, :currency, 'pending', :token)
RETURNING id
"""),
transaction_data
)
transaction_id = transaction.scalar()
# Process refund if provided
if refund_data:
await session.execute(
text("""
INSERT INTO refunds
(transaction_id, amount, currency, reason, status)
VALUES (:transaction_id, :amount, :currency, :reason, 'pending')
"""),
{**refund_data, 'transaction_id': transaction_id}
)
# Update merchant balance atomically
await session.execute(
text("""
UPDATE merchant_balances
SET available_balance = available_balance + :amount
WHERE merchant_id = :merchant_id
"""),
{
'merchant_id': transaction_data['merchant_id'],
'amount': transaction_data['amount']
}
)
return transaction_idReading Time: ~20 minutes