- Shift Elevate
- Posts
- Adapter Pattern: Integrate Legacy Systems with Modern APIs Seamlessly
Adapter Pattern: Integrate Legacy Systems with Modern APIs Seamlessly
The Pain of Incompatible Interfaces
You're building a modern payment processing system for an e-commerce platform. Your sleek new architecture expects clean, standardized interfaces for payment gateways. Then your product manager drops the requirement: "We need to integrate with our legacy payment system that's been running the business for 10 years."
Suddenly, you're staring at two incompatible worlds. Your modern PaymentProcessor interface expects methods like processPayment(PaymentRequest request), while the legacy system only provides makePayment(String cardNumber, String expiryDate, double amount).
The legacy system works perfectly: it processes millions of dollars daily, but its interface doesn't match your modern architecture. Sound familiar?
The Adapter pattern solves this exact headache by creating a bridge between incompatible interfaces, allowing legacy systems to work seamlessly with modern code without modification.
Understanding the Adapter Pattern
The Adapter pattern allows incompatible interfaces to work together by creating a wrapper that translates one interface into another. It acts as a bridge between two incompatible interfaces, making them compatible without modifying their existing code.
Think of it like a power adapter when traveling internationally: your laptop's plug doesn't fit foreign outlets, but an adapter makes them compatible without changing either the laptop or the electrical system.
This pattern promotes interface compatibility and code reusability while maintaining the Open Closed Principle: your existing code remains unchanged.

Adapter pattern components
Core Components
Target Interface: The interface that the client expects to work with.
Adaptee: The existing class with an incompatible interface that needs to be adapted.
Adapter: The bridge class that implements the target interface and translates calls to the adaptee.
Client: The code that uses the target interface without knowing about the adaptation.
Complete Java Implementation
Let's build a payment processing system that demonstrates the Adapter pattern's power in integrating legacy systems:
The Target Interface (Modern System)
public interface PaymentProcessor {
PaymentResult processPayment(PaymentRequest request);
boolean validatePayment(PaymentRequest request);
}
public class PaymentRequest {
private final String cardNumber;
private final String expiryDate;
private final double amount;
// Constructor and getters
public PaymentRequest(String cardNumber, String expiryDate, double amount) {
this.cardNumber = cardNumber;
this.expiryDate = expiryDate;
this.amount = amount;
}
// Getters omitted for brevity
}
public class PaymentResult {
private final boolean success;
private final String transactionId;
private final String message;
public PaymentResult(boolean success, String transactionId, String message) {
this.success = success;
this.transactionId = transactionId;
this.message = message;
}
// Getters omitted for brevity
}
The Adaptee (Legacy System)
// Legacy payment system with incompatible interface
public class LegacyPaymentSystem {
public String makePayment(String cardNumber, String expiryDate, double amount) {
// Legacy payment processing logic
System.out.println("Processing legacy payment...");
System.out.println("Card: " + maskCardNumber(cardNumber));
System.out.println("Amount: $" + amount);
// Simulate processing
if (amount > 0 && cardNumber.length() == 16) {
return "TXN_" + System.currentTimeMillis();
}
return null;
}
public boolean checkCardValidity(String cardNumber, String expiryDate) {
// Legacy validation logic
return cardNumber != null && cardNumber.length() == 16
&& expiryDate != null && expiryDate.matches("\\d{2}/\\d{2}");
}
private String maskCardNumber(String cardNumber) {
return cardNumber.substring(0, 4) + "****" + cardNumber.substring(12);
}
}
The Adapter Implementation
public class LegacyPaymentAdapter implements PaymentProcessor {
private final LegacyPaymentSystem legacySystem;
public LegacyPaymentAdapter(LegacyPaymentSystem legacySystem) {
this.legacySystem = legacySystem;
}
@Override
public PaymentResult processPayment(PaymentRequest request) {
// Translate modern request to legacy format
String transactionId = legacySystem.makePayment(
request.getCardNumber(),
request.getExpiryDate(),
request.getAmount()
);
// Translate legacy response to modern format
if (transactionId != null) {
return new PaymentResult(true, transactionId, "Payment processed successfully");
} else {
return new PaymentResult(false, null, "Payment processing failed");
}
}
@Override
public boolean validatePayment(PaymentRequest request) {
// Translate validation call to legacy system
return legacySystem.checkCardValidity(
request.getCardNumber(),
request.getExpiryDate()
);
}
}
Client Code
public class PaymentService {
private final PaymentProcessor paymentProcessor;
public PaymentService(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public PaymentResult processCustomerPayment(PaymentRequest request) {
// Client code works with modern interface
if (!paymentProcessor.validatePayment(request)) {
return new PaymentResult(false, null, "Invalid payment details");
}
return paymentProcessor.processPayment(request);
}
}
// Client doesn't know about legacy system
public class ECommerceLauncher {
public static void main(String[] args) {
// Legacy system wrapped by adapter
LegacyPaymentSystem legacySystem = new LegacyPaymentSystem();
PaymentProcessor processor = new LegacyPaymentAdapter(legacySystem);
PaymentService paymentService = new PaymentService(processor);
PaymentRequest request = new PaymentRequest(
"1234567890123456",
"12/25",
99.99
);
PaymentResult result = paymentService.processCustomerPayment(request);
System.out.println("Payment successful: " + result.isSuccess());
System.out.println("Transaction ID: " + result.getTransactionId());
}
}
Expected Output:
Processing legacy payment...
Card: 1234****3456
Amount: $99.99
Payment successful: true
Transaction ID: TXN_1640995200000
🚀 Get the Complete Implementation
The full code with multiple adapter implementations and comprehensive testing is available in our Design Patterns Repository.
# Clone and run the complete demo
git clone https://github.com/shift-elevate/design-patterns.git
cd design-patterns
mvn test -Dtest=AdapterPatternTest
Adding New Payment Adapters
The Adapter pattern shines when integrating multiple incompatible systems. The repository demonstrates how adding adapters for different payment gateways becomes straightforward:
Create adapter for each payment system implementing PaymentProcessor
Handle interface translation in each adapter
Client code remains unchanged regardless of underlying system
Switch between systems by simply changing the adapter
This demonstrates loose coupling and high cohesion: your client code depends only on abstractions, not concrete implementations.
Try it yourself: Fork the repository and create adapters for PayPal, Stripe, or other payment systems!
Real World Examples
1. Database Driver Adapters
JDBC drivers are perfect examples of the Adapter pattern. Each database vendor provides an adapter that implements the standard JDBC interfaces:
// Different database drivers adapt to standard JDBC interfaces
Connection mysqlConnection = DriverManager.getConnection(
"jdbc:mysql://localhost/mydb", username, password);
Connection postgresConnection = DriverManager.getConnection(
"jdbc:postgresql://localhost/mydb", username, password);
// Your code uses the same interface regardless of database
PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setInt(1, userId);
ResultSet rs = stmt.executeQuery();
2. Third Party Library Integration
When integrating third party libraries with different APIs, adapters provide a unified interface:
// Adapter for different logging libraries
public class LoggingAdapter implements Logger {
private final ThirdPartyLogger thirdPartyLogger;
public LoggingAdapter(ThirdPartyLogger logger) {
this.thirdPartyLogger = logger;
}
@Override
public void info(String message) {
// Adapt to third-party API
thirdPartyLogger.logInfo(message, getCurrentTimestamp());
}
@Override
public void error(String message, Throwable throwable) {
// Adapt error logging format
thirdPartyLogger.logError(message, throwable.getMessage(),
throwable.getStackTrace());
}
}
3. API Gateway Integration
Modern microservices often use adapters to integrate with external APIs that have different interfaces:
// Adapter for external weather API
public class WeatherServiceAdapter implements WeatherProvider {
private final ExternalWeatherAPI externalAPI;
@Override
public WeatherData getCurrentWeather(String location) {
// Adapt external API response to internal format
ExternalWeatherResponse response = externalAPI.getWeather(location);
return new WeatherData(
response.getTemp(),
response.getHumidity(),
response.getCondition(),
response.getWindSpeed()
);
}
}
When to Use Adapter Pattern
Ideal Scenarios:
Integrating legacy systems with modern architectures.
Working with third party libraries that have incompatible interfaces.
Creating unified interfaces for multiple similar services (payment gateways, notification services).
Migrating from old systems while maintaining backward compatibility.
Building API gateways that need to integrate multiple backend services.
Testing scenarios where you need to mock external dependencies.
Skip It When:
You can modify the incompatible class directly.
The interface differences are minimal and don't justify the adapter overhead.
You're building everything from scratch with control over all interfaces.
Performance is critical and the adapter introduces unacceptable overhead.
Next Steps: Apply Adapter Pattern in Your Project
Identify Integration Points: Look for places where you're struggling to integrate incompatible systems or libraries
Define Target Interfaces: Create clean, modern interfaces that represent what your client code needs
Start with One Adapter: Begin with the most critical integration point and build your first adapter
Test Thoroughly: Ensure the adapter handles all edge cases and error conditions properly
Plan for Multiple Systems: Consider how you'll handle multiple incompatible systems with similar functionality
The Adapter pattern transforms integration nightmares into clean, maintainable solutions. By creating bridges between incompatible interfaces, you build systems that gracefully evolve while preserving valuable existing functionality.
Found this helpful? Share it with a colleague who's struggling with legacy system integration. Have questions about implementing Adapter pattern in your specific use case? Email me directly, we read every message and the best questions become future newsletter topics.