Third-Party Integrations using Adapter Pattern in Django
Learn how to implement the Adapter Pattern in Django to integrate multiple payment gateways (Stripe, PayFast, JazzCash) with clean, swappable code.
The Adapter Pattern in Django: Integrating External Services Without the Mess
Integrating third-party APIs directly into Django views or models is one of the fastest ways to accumulate technical debt. When an application directly calls vendor SDKs, changes to an external API format or adding a secondary provider can break business logic across multiple apps.
The Adapter Pattern solves this by introducing an intermediary layer that translates differing external interfaces into a single, predictable contract that your Django application expects.
Adapter Pattern structure translating client calls. Source: GeeksforGeeks
The Problem: Tight Coupling to Third-Party SDKs
Consider a Django checkout flow that accepts payments. A direct implementation often looks like this:
# views.py (Anti-pattern)
import stripe
from django.conf import settings
def process_checkout(request):
# Direct coupling to Stripe's specific parameters and exceptions
try:
charge = stripe.Charge.create(
amount=int(request.POST['amount']) * 100,
currency="usd",
source=request.POST['stripe_token']
)
except stripe.error.CardError as e:
...If you need to introduce local payment gateways like JazzCash or PayFast, your views quickly become cluttered with nested if/elif statements, different parameter conversions, and vendor-specific error handling.
Step 1: Define the Unified Interface (Target)
Use Python's built-in abc module to define the contract that all payment gateways must fulfill.
# services/payments/base.py
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
@dataclass
class PaymentResult:
success: bool
transaction_id: Optional[str]
error_message: Optional[str] = None
class PaymentProvider(ABC):
"""Target Interface: The standard contract expected by the app."""
@abstractmethod
def charge(self, amount: float, source: str, currency: str = "PKR") -> PaymentResult:
"""Charge the given amount and return a standardized PaymentResult."""
pass
@abstractmethod
def refund(self, transaction_id: str, amount: float) -> PaymentResult:
"""Refund a previous charge."""
passStep 2: Implement Concrete Adapters
Each adapter encapsulates provider-specific SDK logic, parameter transformations, and exception handling into the unified PaymentResult output format.
# services/payments/adapters.py
import stripe
import requests
from django.conf import settings
from .base import PaymentProvider, PaymentResult
class StripeAdapter(PaymentProvider):
def __init__(self):
stripe.api_key = settings.STRIPE_SECRET_KEY
def charge(self, amount: float, source: str, currency: str = "USD") -> PaymentResult:
try:
# Stripe expects amounts in the smallest currency unit (cents)
charge = stripe.Charge.create(
amount=int(amount * 100),
currency=currency.lower(),
source=source,
)
return PaymentResult(success=True, transaction_id=charge.id)
except stripe.error.StripeError as e:
return PaymentResult(success=False, transaction_id=None, error_message=str(e))
def refund(self, transaction_id: str, amount: float) -> PaymentResult:
try:
refund = stripe.Refund.create(charge=transaction_id, amount=int(amount * 100))
return PaymentResult(success=True, transaction_id=refund.id)
except stripe.error.StripeError as e:
return PaymentResult(success=False, transaction_id=None, error_message=str(e))
class JazzCashAdapter(PaymentProvider):
def __init__(self):
self.merchant_id = settings.JAZZCASH_MERCHANT_ID
self.password = settings.JAZZCASH_PASSWORD
self.endpoint = settings.JAZZCASH_API_URL
def charge(self, amount: float, source: str, currency: str = "PKR") -> PaymentResult:
payload = {
"pp_MerchantID": self.merchant_id,
"pp_Password": self.password,
"pp_Amount": f"{amount:.2f}",
"pp_MobileNumber": source,
}
response = requests.post(f"{self.endpoint}/charge", json=payload, timeout=10)
data = response.json()
if response.status_code == 200 and data.get("pp_ResponseCode") == "000":
return PaymentResult(success=True, transaction_id=data.get("pp_TxnRefNo"))
return PaymentResult(
success=False,
transaction_id=None,
error_message=data.get("pp_ResponseMessage", "Transaction failed")
)
def refund(self, transaction_id: str, amount: float) -> PaymentResult:
# Provider-specific refund API logic
...
class PayFastAdapter(PaymentProvider):
def charge(self, amount: float, source: str, currency: str = "PKR") -> PaymentResult:
# PayFast API specific integration
...
def refund(self, transaction_id: str, amount: float) -> PaymentResult:
...Step 3: Integrate with Factory and Service Layers
Combine the adapter with a factory to instantiate the correct provider dynamically based on user input or tenant configuration.
# services/payments/factory.py
from .base import PaymentProvider
from .adapters import StripeAdapter, JazzCashAdapter, PayFastAdapter
class PaymentProviderFactory:
_registry = {
"stripe": StripeAdapter,
"jazzcash": JazzCashAdapter,
"payfast": PayFastAdapter,
}
@classmethod
def get_provider(cls, provider_name: str) -> PaymentProvider:
adapter_cls = cls._registry.get(provider_name.lower())
if not adapter_cls:
raise ValueError(f"Unsupported payment provider: {provider_name}")
return adapter_cls()Now, consuming the service in your Django views or domain services requires zero knowledge of third-party implementation details:
# services/order_service.py
from .payments.factory import PaymentProviderFactory
class OrderCheckoutService:
@staticmethod
def process_order_payment(order, provider_name: str, payment_token: str) -> bool:
provider = PaymentProviderFactory.get_provider(provider_name)
result = provider.charge(
amount=order.total_amount,
source=payment_token,
currency="PKR"
)
if result.success:
order.status = "PAID"
order.transaction_id = result.transaction_id
order.save()
return True
else:
order.status = "FAILED"
order.error_log = result.error_message
order.save()
return FalseAdopting the Adapter Pattern keeps your application core insulated against third-party volatility, reduces vendor lock-in, and lets you add new service providers with zero regressions to existing code.