Structural Pattern

Adapter

Allows objects with incompatible interfaces to work together.

Adapter Pattern: A Complete Guide with Examples in 11 Programming Languages

The Adapter Pattern is a structural design pattern that allows incompatible interfaces to work together by acting as a bridge between them. It converts the interface of a class into another interface that clients expect, enabling classes with incompatible interfaces to collaborate. Whether you're integrating third-party libraries, working with legacy code, or creating flexible APIs, the Adapter Pattern provides a clean solution for interface incompatibility without modifying existing code.

In this comprehensive guide, we'll explore the Adapter Pattern with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin - complete with language-specific best practices, common pitfalls, and when to use this essential pattern.

Table of Contents

What is the Adapter Pattern?

The Adapter Pattern allows objects with incompatible interfaces to collaborate. It wraps an object with an incompatible interface and exposes a compatible interface that clients can use. The adapter translates calls from the client into calls that the wrapped object understands.

Think of it like a power adapter for international travel. Your American device has a two-prong plug (interface), but European outlets have different holes (incompatible interface). The power adapter (Adapter Pattern) sits between them, letting your device work with European outlets without modifying either the device or the outlet.

Why Use the Adapter Pattern?

The Adapter Pattern offers several compelling benefits:

  1. Reusability: Use existing classes even if their interfaces don't match
  2. Open/Closed Principle: Add new adapters without modifying existing code
  3. Single Responsibility: Adapter handles interface conversion, not business logic
  4. Legacy Integration: Integrate old code with new systems without rewriting
  5. Third-Party Libraries: Wrap external libraries to match your interfaces
  6. Testability: Easy to mock adapters for testing
  7. Flexibility: Switch between different implementations through adapters

Adapter Pattern Comparison

Let's compare Adapter with related structural patterns:

PatternPurposeWhen to UseModifies Interface
AdapterMake incompatible interfaces compatibleIntegrate existing classes with different interfacesYes - converts interface
FacadeSimplify complex subsystemProvide simple interface to complex systemNo - adds new interface
DecoratorAdd responsibilities dynamicallyExtend functionality without subclassingNo - same interface
ProxyControl access to objectAdd access control, lazy loading, loggingNo - same interface
BridgeSeparate abstraction from implementationVary abstraction and implementation independentlyNo - parallel hierarchies

Key Distinction: Adapter changes an existing interface to make it compatible, while other patterns work with compatible interfaces.

Adapter Pattern Explained

The Adapter Pattern has two main variants:

1. Class Adapter (Inheritance-based)

Uses multiple inheritance to adapt one interface to another.

2. Object Adapter (Composition-based)

Uses composition - adapter contains an instance of the adapted class.

Note: Most languages don't support multiple inheritance, so Object Adapter is more common.

Core Components:

  1. Target Interface: The interface clients expect
  2. Adaptee: The existing class with incompatible interface
  3. Adapter: Converts Adaptee's interface to Target interface
  4. Client: Uses Target interface to interact with objects

Class Diagram

Here's the UML class diagram showing the Adapter structure:

Beginner-Friendly Example

Let's start with a simple example: payment gateway integration. Different payment processors have different APIs, but we want a unified interface.

from abc import ABC, abstractmethod
from typing import Dict

# Target interface - what our application expects
class PaymentProcessor(ABC):
    """Unified payment interface"""
    
    @abstractmethod
    def process_payment(self, amount: float, currency: str) -> Dict[str, any]:
        """Process a payment and return result"""
        pass

# Adaptee 1 - Stripe API (third-party)
class StripeAPI:
    """Stripe's existing API with different interface"""
    
    def create_charge(self, amount_cents: int, currency: str, token: str) -> Dict:
        """Stripe's method signature is different"""
        print(f"Stripe: Charging {amount_cents/100} {currency}")
        return {
            'id': 'ch_stripe123',
            'status': 'succeeded',
            'amount': amount_cents
        }

# Adaptee 2 - PayPal API (third-party)
class PayPalAPI:
    """PayPal's existing API with different interface"""
    
    def make_payment(self, total: float, currency_code: str) -> Dict:
        """PayPal's method signature is different"""
        print(f"PayPal: Processing payment of {total} {currency_code}")
        return {
            'transaction_id': 'pp_12345',
            'state': 'approved',
            'total': total
        }

# Adapter 1 - Stripe Adapter
class StripeAdapter(PaymentProcessor):
    """Adapts Stripe API to our PaymentProcessor interface"""
    
    def __init__(self, stripe_api: StripeAPI):
        self.stripe = stripe_api
    
    def process_payment(self, amount: float, currency: str) -> Dict[str, any]:
        # Convert our interface to Stripe's interface
        amount_cents = int(amount * 100)
        token = "tok_visa"  # Simplified
        
        # Call Stripe's method
        result = self.stripe.create_charge(amount_cents, currency, token)
        
        # Convert Stripe's response to our format
        return {
            'success': result['status'] == 'succeeded',
            'transaction_id': result['id'],
            'amount': amount,
            'provider': 'stripe'
        }

# Adapter 2 - PayPal Adapter
class PayPalAdapter(PaymentProcessor):
    """Adapts PayPal API to our PaymentProcessor interface"""
    
    def __init__(self, paypal_api: PayPalAPI):
        self.paypal = paypal_api
    
    def process_payment(self, amount: float, currency: str) -> Dict[str, any]:
        # Call PayPal's method with their parameter names
        result = self.paypal.make_payment(amount, currency)
        
        # Convert PayPal's response to our format
        return {
            'success': result['state'] == 'approved',
            'transaction_id': result['transaction_id'],
            'amount': amount,
            'provider': 'paypal'
        }

# Client code
class PaymentService:
    """Client that uses PaymentProcessor interface"""
    
    def __init__(self, processor: PaymentProcessor):
        self.processor = processor
    
    def checkout(self, amount: float, currency: str):
        print(f"\nProcessing checkout: {amount} {currency}")
        result = self.processor.process_payment(amount, currency)
        
        if result['success']:
            print(f"āœ“ Payment successful!")
            print(f"  Transaction ID: {result['transaction_id']}")
            print(f"  Provider: {result['provider']}")
        else:
            print(f"āœ— Payment failed")
        
        return result

# Usage
if __name__ == "__main__":
    print("="*60)
    print("Payment Gateway Adapter Pattern Demo")
    print("="*60)
    
    # Use Stripe
    stripe_api = StripeAPI()
    stripe_adapter = StripeAdapter(stripe_api)
    payment_service = PaymentService(stripe_adapter)
    payment_service.checkout(99.99, "USD")
    
    # Switch to PayPal - same interface!
    paypal_api = PayPalAPI()
    paypal_adapter = PayPalAdapter(paypal_api)
    payment_service = PaymentService(paypal_adapter)
    payment_service.checkout(149.99, "USD")

Production-Ready Example

Now let's look at a more realistic, production-ready implementation: a multi-database adapter system that provides a unified interface for different database engines with connection pooling, error handling, and query optimization.

šŸ Python (Production Example)

from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional
import logging
from dataclasses import dataclass
from enum import Enum
import time

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Enums
class DatabaseType(Enum):
    POSTGRESQL = "postgresql"
    MYSQL = "mysql"
    MONGODB = "mongodb"

# Data classes
@dataclass
class QueryResult:
    success: bool
    rows: List[Dict[str, Any]]
    affected_rows: int
    execution_time: float
    error: Optional[str] = None

@dataclass
class ConnectionConfig:
    host: str
    port: int
    database: str
    username: str
    password: str
    pool_size: int = 10

# Target interface - Unified database interface
class DatabaseAdapter(ABC):
    """Unified database interface that all adapters must implement"""
    
    @abstractmethod
    def connect(self) -> bool:
        """Establish database connection"""
        pass
    
    @abstractmethod
    def disconnect(self) -> bool:
        """Close database connection"""
        pass
    
    @abstractmethod
    def execute_query(self, query: str, params: Optional[Dict] = None) -> QueryResult:
        """Execute a query and return results"""
        pass
    
    @abstractmethod
    def execute_batch(self, queries: List[str]) -> List[QueryResult]:
        """Execute multiple queries in a batch"""
        pass
    
    @abstractmethod
    def get_connection_status(self) -> Dict[str, Any]:
        """Get connection status and statistics"""
        pass

# Adaptee 1 - PostgreSQL Driver (third-party)
class PostgreSQLDriver:
    """Simulated PostgreSQL driver with different interface"""
    
    def __init__(self, config: Dict):
        self.config = config
        self.connected = False
        self.query_count = 0
    
    def pg_connect(self, host: str, port: int, db: str, user: str, pwd: str):
        logger.info(f"PostgreSQL: Connecting to {host}:{port}/{db}")
        self.connected = True
        return {"status": "connected", "connection_id": "pg_12345"}
    
    def pg_query(self, sql: str, bind_vars: Dict = None):
        self.query_count += 1
        logger.info(f"PostgreSQL: Executing query - {sql[:50]}...")
        time.sleep(0.01)  # Simulate query time
        
        return {
            "cursor": [{"id": 1, "name": "PostgreSQL Result"}],
            "rowcount": 1,
            "time_ms": 10
        }
    
    def pg_close(self):
        logger.info("PostgreSQL: Connection closed")
        self.connected = False

# Adaptee 2 - MySQL Driver (third-party)
class MySQLDriver:
    """Simulated MySQL driver with different interface"""
    
    def __init__(self):
        self.connection = None
        self.stats = {"queries": 0, "errors": 0}
    
    def mysql_connect(self, **kwargs):
        logger.info(f"MySQL: Connecting to {kwargs.get('host')}:{kwargs.get('port')}")
        self.connection = {"id": "mysql_67890", "active": True}
        return self.connection
    
    def mysql_execute(self, statement: str, parameters: tuple = ()):
        self.stats["queries"] += 1
        logger.info(f"MySQL: Executing - {statement[:50]}...")
        time.sleep(0.01)
        
        return {
            "data": [{"id": 2, "name": "MySQL Result"}],
            "affected": 1,
            "duration": 0.01
        }
    
    def mysql_disconnect(self):
        logger.info("MySQL: Disconnected")
        self.connection = None

# Adaptee 3 - MongoDB Driver (third-party)
class MongoDBDriver:
    """Simulated MongoDB driver with different interface"""
    
    def __init__(self, connection_string: str):
        self.conn_string = connection_string
        self.db = None
        self.operation_count = 0
    
    def mongo_connect(self):
        logger.info(f"MongoDB: Connecting to {self.conn_string}")
        self.db = {"name": "mongodb", "collections": []}
        return {"acknowledged": True}
    
    def mongo_find(self, collection: str, filter_doc: Dict = None):
        self.operation_count += 1
        logger.info(f"MongoDB: Finding in {collection}")
        time.sleep(0.01)
        
        return {
            "documents": [{"_id": "abc123", "name": "MongoDB Result"}],
            "count": 1
        }
    
    def mongo_close(self):
        logger.info("MongoDB: Connection closed")
        self.db = None

# Adapter 1 - PostgreSQL Adapter
class PostgreSQLAdapter(DatabaseAdapter):
    """Adapts PostgreSQL driver to unified interface"""
    
    def __init__(self, config: ConnectionConfig):
        self.config = config
        self.driver = PostgreSQLDriver({})
        self.connected = False
    
    def connect(self) -> bool:
        try:
            result = self.driver.pg_connect(
                host=self.config.host,
                port=self.config.port,
                db=self.config.database,
                user=self.config.username,
                pwd=self.config.password
            )
            self.connected = result["status"] == "connected"
            return self.connected
        except Exception as e:
            logger.error(f"PostgreSQL connection failed: {e}")
            return False
    
    def disconnect(self) -> bool:
        try:
            self.driver.pg_close()
            self.connected = False
            return True
        except Exception as e:
            logger.error(f"PostgreSQL disconnect failed: {e}")
            return False
    
    def execute_query(self, query: str, params: Optional[Dict] = None) -> QueryResult:
        start_time = time.time()
        
        try:
            result = self.driver.pg_query(query, params or {})
            execution_time = time.time() - start_time
            
            return QueryResult(
                success=True,
                rows=result["cursor"],
                affected_rows=result["rowcount"],
                execution_time=execution_time
            )
        except Exception as e:
            execution_time = time.time() - start_time
            logger.error(f"PostgreSQL query failed: {e}")
            return QueryResult(
                success=False,
                rows=[],
                affected_rows=0,
                execution_time=execution_time,
                error=str(e)
            )
    
    def execute_batch(self, queries: List[str]) -> List[QueryResult]:
        return [self.execute_query(q) for q in queries]
    
    def get_connection_status(self) -> Dict[str, Any]:
        return {
            "type": "PostgreSQL",
            "connected": self.connected,
            "queries_executed": self.driver.query_count,
            "host": self.config.host,
            "database": self.config.database
        }

# Adapter 2 - MySQL Adapter
class MySQLAdapter(DatabaseAdapter):
    """Adapts MySQL driver to unified interface"""
    
    def __init__(self, config: ConnectionConfig):
        self.config = config
        self.driver = MySQLDriver()
        self.connected = False
    
    def connect(self) -> bool:
        try:
            self.driver.mysql_connect(
                host=self.config.host,
                port=self.config.port,
                database=self.config.database,
                user=self.config.username,
                password=self.config.password
            )
            self.connected = True
            return True
        except Exception as e:
            logger.error(f"MySQL connection failed: {e}")
            return False
    
    def disconnect(self) -> bool:
        try:
            self.driver.mysql_disconnect()
            self.connected = False
            return True
        except Exception as e:
            logger.error(f"MySQL disconnect failed: {e}")
            return False
    
    def execute_query(self, query: str, params: Optional[Dict] = None) -> QueryResult:
        start_time = time.time()
        
        try:
            # Convert dict params to tuple for MySQL
            param_tuple = tuple(params.values()) if params else ()
            result = self.driver.mysql_execute(query, param_tuple)
            execution_time = time.time() - start_time
            
            return QueryResult(
                success=True,
                rows=result["data"],
                affected_rows=result["affected"],
                execution_time=execution_time
            )
        except Exception as e:
            execution_time = time.time() - start_time
            logger.error(f"MySQL query failed: {e}")
            return QueryResult(
                success=False,
                rows=[],
                affected_rows=0,
                execution_time=execution_time,
                error=str(e)
            )
    
    def execute_batch(self, queries: List[str]) -> List[QueryResult]:
        return [self.execute_query(q) for q in queries]
    
    def get_connection_status(self) -> Dict[str, Any]:
        return {
            "type": "MySQL",
            "connected": self.connected,
            "queries_executed": self.driver.stats["queries"],
            "errors": self.driver.stats["errors"],
            "host": self.config.host,
            "database": self.config.database
        }

# Adapter 3 - MongoDB Adapter
class MongoDBAdapter(DatabaseAdapter):
    """Adapts MongoDB driver to unified interface"""
    
    def __init__(self, config: ConnectionConfig):
        self.config = config
        conn_string = f"mongodb://{config.username}:{config.password}@{config.host}:{config.port}/{config.database}"
        self.driver = MongoDBDriver(conn_string)
        self.connected = False
    
    def connect(self) -> bool:
        try:
            self.driver.mongo_connect()
            self.connected = True
            return True
        except Exception as e:
            logger.error(f"MongoDB connection failed: {e}")
            return False
    
    def disconnect(self) -> bool:
        try:
            self.driver.mongo_close()
            self.connected = False
            return True
        except Exception as e:
            logger.error(f"MongoDB disconnect failed: {e}")
            return False
    
    def execute_query(self, query: str, params: Optional[Dict] = None) -> QueryResult:
        """
        Translate SQL-like queries to MongoDB operations
        This is simplified - real implementation would parse query
        """
        start_time = time.time()
        
        try:
            # Simplified: assume query contains collection name
            collection = "default_collection"
            filter_doc = params or {}
            
            result = self.driver.mongo_find(collection, filter_doc)
            execution_time = time.time() - start_time
            
            return QueryResult(
                success=True,
                rows=result["documents"],
                affected_rows=result["count"],
                execution_time=execution_time
            )
        except Exception as e:
            execution_time = time.time() - start_time
            logger.error(f"MongoDB query failed: {e}")
            return QueryResult(
                success=False,
                rows=[],
                affected_rows=0,
                execution_time=execution_time,
                error=str(e)
            )
    
    def execute_batch(self, queries: List[str]) -> List[QueryResult]:
        return [self.execute_query(q) for q in queries]
    
    def get_connection_status(self) -> Dict[str, Any]:
        return {
            "type": "MongoDB",
            "connected": self.connected,
            "operations": self.driver.operation_count,
            "host": self.config.host,
            "database": self.config.database
        }

# Database Factory (uses adapters)
class DatabaseFactory:
    """Factory to create appropriate database adapter"""
    
    @staticmethod
    def create_adapter(db_type: DatabaseType, config: ConnectionConfig) -> DatabaseAdapter:
        adapters = {
            DatabaseType.POSTGRESQL: PostgreSQLAdapter,
            DatabaseType.MYSQL: MySQLAdapter,
            DatabaseType.MONGODB: MongoDBAdapter
        }
        
        adapter_class = adapters.get(db_type)
        if not adapter_class:
            raise ValueError(f"Unsupported database type: {db_type}")
        
        return adapter_class(config)

# Client - Data Access Layer
class DataAccessLayer:
    """High-level data access layer using unified interface"""
    
    def __init__(self, adapter: DatabaseAdapter):
        self.db = adapter
    
    def initialize(self):
        """Initialize database connection"""
        if self.db.connect():
            logger.info("Database connection established")
            return True
        else:
            logger.error("Database connection failed")
            return False
    
    def get_users(self) -> List[Dict]:
        """Get all users - works with any database"""
        query = "SELECT * FROM users"
        result = self.db.execute_query(query)
        
        if result.success:
            logger.info(f"Retrieved {len(result.rows)} users in {result.execution_time:.3f}s")
            return result.rows
        else:
            logger.error(f"Failed to retrieve users: {result.error}")
            return []
    
    def create_user(self, name: str, email: str) -> bool:
        """Create a user - works with any database"""
        query = "INSERT INTO users (name, email) VALUES (:name, :email)"
        params = {"name": name, "email": email}
        
        result = self.db.execute_query(query, params)
        
        if result.success:
            logger.info(f"User created: {name}")
            return True
        else:
            logger.error(f"Failed to create user: {result.error}")
            return False
    
    def close(self):
        """Close database connection"""
        self.db.disconnect()

# Usage Example
def demonstrate_database_adapters():
    """Demonstrate multi-database adapter system"""
    
    print("="*60)
    print("Multi-Database Adapter System")
    print("="*60 + "\n")
    
    # Configuration for different databases
    pg_config = ConnectionConfig(
        host="localhost",
        port=5432,
        database="myapp",
        username="postgres",
        password="secret"
    )
    
    mysql_config = ConnectionConfig(
        host="localhost",
        port=3306,
        database="myapp",
        username="root",
        password="secret"
    )
    
    mongo_config = ConnectionConfig(
        host="localhost",
        port=27017,
        database="myapp",
        username="admin",
        password="secret"
    )
    
    # Test with PostgreSQL
    print("Testing with PostgreSQL:")
    print("-" * 40)
    pg_adapter = DatabaseFactory.create_adapter(DatabaseType.POSTGRESQL, pg_config)
    dal_pg = DataAccessLayer(pg_adapter)
    dal_pg.initialize()
    dal_pg.get_users()
    dal_pg.create_user("Alice", "alice@example.com")
    print(f"Status: {pg_adapter.get_connection_status()}")
    dal_pg.close()
    
    # Test with MySQL
    print("\nTesting with MySQL:")
    print("-" * 40)
    mysql_adapter = DatabaseFactory.create_adapter(DatabaseType.MYSQL, mysql_config)
    dal_mysql = DataAccessLayer(mysql_adapter)
    dal_mysql.initialize()
    dal_mysql.get_users()
    dal_mysql.create_user("Bob", "bob@example.com")
    print(f"Status: {mysql_adapter.get_connection_status()}")
    dal_mysql.close()
    
    # Test with MongoDB
    print("\nTesting with MongoDB:")
    print("-" * 40)
    mongo_adapter = DatabaseFactory.create_adapter(DatabaseType.MONGODB, mongo_config)
    dal_mongo = DataAccessLayer(mongo_adapter)
    dal_mongo.initialize()
    dal_mongo.get_users()
    dal_mongo.create_user("Charlie", "charlie@example.com")
    print(f"Status: {mongo_adapter.get_connection_status()}")
    dal_mongo.close()
    
    print("\n" + "="*60)
    print("Same interface, different implementations!")
    print("="*60)

if __name__ == "__main__":
    demonstrate_database_adapters()

Key Production Features:

  1. Unified Interface: Single DatabaseAdapter interface for all databases
  2. Error Handling: Comprehensive try-catch with logging
  3. Connection Management: Proper connect/disconnect lifecycle
  4. Performance Tracking: Execution time measurement
  5. Statistics: Query counts and connection status
  6. Configuration: Centralized connection config
  7. Factory Pattern: Easy database selection
  8. Type Safety: Enums and dataclasses
  9. Batch Operations: Support for multiple queries
  10. Production-Ready: Logging, error messages, status reporting

Real-World Use Cases

The Adapter Pattern is essential in many real-world scenarios:

1. Payment Gateway Integration

Unify different payment processors:

StripeAdapter, PayPalAdapter, SquareAdapter → Unified PaymentProcessor

Examples: E-commerce platforms, billing systems, fintech apps Industries: E-commerce, SaaS, financial services

2. Database Abstraction Layers

Support multiple database engines:

PostgreSQLAdapter, MySQLAdapter, MongoDBAdapter → Unified DatabaseInterface

Examples: ORMs (Hibernate, SQLAlchemy), multi-tenant systems Industries: Enterprise software, cloud platforms, data analytics

3. Cloud Provider Abstraction

Work with multiple cloud providers:

AWSAdapter, AzureAdapter, GCPAdapter → Unified CloudStorage

Examples: Multi-cloud platforms, cloud migration tools Industries: DevOps, cloud infrastructure, SaaS platforms

4. Logging Systems

Unify different logging backends:

FileLoggerAdapter, SyslogAdapter, CloudWatchAdapter → Unified Logger

Examples: Centralized logging, monitoring platforms Industries: Observability, enterprise applications, microservices

5. API Versioning

Support multiple API versions:

APIv1Adapter, APIv2Adapter, APIv3Adapter → Unified APIInterface

Examples: REST APIs, GraphQL gateways, API management Industries: Web services, mobile backends, integration platforms

6. Legacy System Integration

Integrate old code with new systems:

LegacySystemAdapter → Modern interface

Examples: Mainframe integration, COBOL wrappers, legacy migration Industries: Banking, insurance, government systems

7. Third-Party Library Wrappers

Wrap external libraries for testability:

HttpClientAdapter wraps Axios/Fetch → Unified HTTP interface

Examples: Testing, dependency isolation, library swapping Industries: All software development

8. Message Queue Integration

Support different message brokers:

KafkaAdapter, RabbitMQAdapter, SQSAdapter → Unified MessageQueue

Examples: Event-driven architectures, microservices communication Industries: Distributed systems, real-time processing, IoT

Language-Specific Mistakes and Anti-Patterns

āŒ Mistake #1: Not Using ABC for Target Interface

# BAD: No enforcement
class PaymentProcessor:
    def process(self, amount):
        pass  # Subclasses might forget

# GOOD: Use ABC
from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    @abstractmethod
    def process(self, amount):
        pass  # Must be implemented

āŒ Mistake #2: Adapter Doing Business Logic

# BAD: Adapter contains business logic
class StripeAdapter:
    def process(self, amount):
        if amount > 1000:  # Business logic!
            amount *= 0.9  # Discount
        return self.stripe.charge(amount)

# GOOD: Adapter only translates interface
class StripeAdapter:
    def process(self, amount):
        return self.stripe.charge(amount)  # Just translation

āŒ Mistake #3: Tight Coupling to Adaptee

# BAD: Adapter creates adaptee
class StripeAdapter:
    def __init__(self, api_key):
        self.stripe = StripeAPI(api_key)  # Creates dependency

# GOOD: Inject adaptee
class StripeAdapter:
    def __init__(self, stripe_api):
        self.stripe = stripe_api  # Dependency injection

Frequently Asked Questions

Q1: Adapter vs Facade - What's the difference?

Adapter: Converts one interface to another to make incompatible interfaces work together. One-to-one relationship.

Facade: Simplifies a complex subsystem by providing a simple interface. One-to-many relationship.

# Adapter - interface conversion
class StripeAdapter(PaymentProcessor):
    def process(self, amount):
        return self.stripe.charge(amount * 100)  # Converts interface

# Facade - simplification
class PaymentFacade:
    def __init__(self):
        self.stripe = Stripe()
        self.validator = Validator()
        self.logger = Logger()
    
    def pay(self, amount):
        self.validator.validate(amount)
        result = self.stripe.charge(amount)
        self.logger.log(result)
        return result  # Simplifies multiple subsystems

Q2: When should I use class adapter vs object adapter?

Object Adapter (Composition) - Preferred:

  • More flexible (can adapt multiple adaptees)
  • Works in languages without multiple inheritance
  • Follows "composition over inheritance"

Class Adapter (Inheritance) - Rarely used:

  • Only if language supports multiple inheritance
  • Slightly more efficient (no indirection)
  • Can override adaptee behavior

Recommendation: Always use object adapter unless you have a specific reason not to.

Q3: Can one adapter adapt multiple adaptees?

Yes! This is common for creating unified interfaces:

class UnifiedDatabaseAdapter:
    def __init__(self, sql_db, nosql_db):
        self.sql = sql_db
        self.nosql = nosql_db
    
    def query(self, query_type, data):
        if query_type == "relational":
            return self.sql.execute(data)
        else:
            return self.nosql.find(data)

Q4: Should adapters be stateless?

Prefer stateless when possible:

  • Easier to test
  • No shared state issues
  • Can be reused

Stateful is okay when:

  • Caching results
  • Connection pooling
  • Performance optimization

Just ensure thread safety if stateful.

Q5: How do I test code using adapters?

Strategy 1: Mock the adapter

def test_payment_service():
    mock_adapter = Mock(spec=PaymentProcessor)
    mock_adapter.process.return_value = PaymentResult(success=True)
    
    service = PaymentService(mock_adapter)
    result = service.checkout(99.99)
    
    assert result.success
    mock_adapter.process.assert_called_once()

Strategy 2: Create test adapter

class TestPaymentAdapter(PaymentProcessor):
    def process(self, amount):
        return PaymentResult(success=True, id="test_123")

def test_with_test_adapter():
    service = PaymentService(TestPaymentAdapter())
    assert service.checkout(99.99).success

Q6: Can adapters change behavior, not just interface?

Adapters should only translate interface, not add business logic:

# BAD: Adapter adds behavior
class DiscountAdapter(PaymentProcessor):
    def process(self, amount):
        discounted = amount * 0.9  # Business logic!
        return self.stripe.charge(discounted)

# GOOD: Keep adapter simple
class StripeAdapter(PaymentProcessor):
    def process(self, amount):
        return self.stripe.charge(amount)  # Just translation

# Put business logic in service
class PaymentService:
    def checkout(self, amount, discount=0):
        final_amount = amount * (1 - discount)  # Business logic here
        return self.adapter.process(final_amount)

Q7: How do I handle versioning with adapters?

Create separate adapters for each version:

class APIv1Adapter(APIInterface):
    def get_user(self, user_id):
        return self.api.users.get(id=user_id)  # v1 syntax

class APIv2Adapter(APIInterface):
    def get_user(self, user_id):
        return self.api.fetch_user(user_id)  # v2 syntax

# Factory selects appropriate adapter
def get_adapter(version):
    if version == 1:
        return APIv1Adapter(APIv1Client())
    else:
        return APIv2Adapter(APIv2Client())

Q8: Should I create adapters for all third-party libraries?

Not always. Create adapters when:

  • Library's interface doesn't match your domain
  • You might switch libraries later
  • Testing requires isolation
  • Library is complex or unstable

Skip adapters when:

  • Library already matches your needs
  • One-time use
  • Library is stable and unlikely to change
  • Performance is critical (avoid extra layer)

Q9: How do I handle errors from adaptees?

Translate exceptions to domain exceptions:

class PaymentAdapter:
    def process(self, amount):
        try:
            return self.stripe.charge(amount)
        except StripeNetworkError as e:
            raise PaymentNetworkError("Network failed") from e
        except StripeInvalidCard as e:
            raise PaymentValidationError("Invalid card") from e
        except Exception as e:
            raise PaymentError("Payment failed") from e

This way clients don't depend on third-party exception types.

Q10: Can adapters be bidirectional?

Yes, though rare. Bidirectional adapters translate in both directions:

class BidirectionalAdapter:
    def to_external(self, internal_data):
        # Internal format → External format
        return ExternalFormat(internal_data.value)
    
    def to_internal(self, external_data):
        # External format → Internal format
        return InternalFormat(external_data.value)

Usually you create two separate adapters instead.

Key Takeaways

šŸŽÆ Core Concept: Adapter Pattern makes incompatible interfaces work together by converting one interface into another that clients expect. It's a bridge between incompatible systems.

šŸ”‘ Key Benefits:

  • Reusability: Use existing classes with incompatible interfaces
  • Flexibility: Switch implementations easily
  • Open/Closed: Add new adapters without modifying existing code
  • Testability: Easy to mock adapters for testing
  • Integration: Perfect for third-party libraries and legacy systems

🌐 Implementation Variants:

  • Object Adapter (composition): Preferred, more flexible
  • Class Adapter (inheritance): Rarely used, requires multiple inheritance

šŸ“‹ When to Use Adapter:

  • Integrating third-party libraries
  • Working with legacy code
  • Creating reusable components
  • Need to switch between implementations
  • Different API versions

āš ļø When NOT to Use Adapter:

  • Interfaces already compatible
  • Can modify source code directly
  • Simple one-off integration
  • Performance is critical (extra layer adds overhead)

šŸ› ļø Best Practices:

  1. Keep adapters simple: Only translate interface, no business logic
  2. Use composition: Prefer object adapter over class adapter
  3. Translate exceptions: Convert third-party exceptions to domain exceptions
  4. Make stateless: Avoid adapter state when possible
  5. Document adaptation: Explain what's being adapted and why
  6. Test thoroughly: Test both adapter and integrated system
  7. Version carefully: Create separate adapters for different versions

šŸ’” Common Use Cases:

  • Payment gateway integration
  • Database abstraction layers
  • Cloud provider abstraction
  • API versioning
  • Legacy system modernization
  • Third-party library wrapping
  • Message queue integration
  • Logging system unification

The Adapter Pattern is one of the most practical structural patterns. Use it whenever you need to make incompatible interfaces work together, but remember to keep adapters focused on interface translation, not business logic.


Want to dive deeper into design patterns? Check out our comprehensive guides on: