DevHuzaifa's Portfolio
2 min readUpdated Aug 17, 2026

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.

#Django#System Design#Code Patterns

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

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:

python
# 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.

python
# 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."""
        pass

Step 2: Implement Concrete Adapters

Each adapter encapsulates provider-specific SDK logic, parameter transformations, and exception handling into the unified PaymentResult output format.

python
# 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.

python
# 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:

python
# 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 False

Adopting 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.