Adapter Pattern: Advanced Variations and Pitfalls
Beyond basic implementation, the Adapter Pattern has several variations and common pitfalls that separate junior from senior engineers in interviews.
Variation 1: Pluggable Adapters
Instead of hardcoding adapter selection, use a registry pattern where adapters register themselves for specific types. This enables runtime discovery and plugin architectures.
- adapters: Map<String, PaymentAdapter>
+ register(type: String, adapter: PaymentAdapter)
+ getAdapter(type: String): PaymentAdapter
registry.register("PAYPAL", new PayPalAdapter())
registry.register("STRIPE", new StripeAdapter())
adapter = registry.getAdapter(userSelection)
adapter.processPayment(amount)
Use Case: E-commerce platforms where merchants can enable/disable payment gateways through configuration without code changes. Each gateway adapter registers itself on application startup.
Variation 2: Adapter Chain
Sometimes you need multiple levels of adaptation. For example, adapting a REST API response to a domain object, then adapting that domain object to a different domain model.
LegacyDomainAdapter → LegacyDomainObject
However, long adapter chains indicate design problems. If you need more than two levels, consider refactoring to a unified model or using a dedicated transformation service.
Common Pitfall 1: Bloated Adapters
Example: A payment adapter should only translate processPayment() to the gateway's API. Currency conversion, fraud detection, transaction logging, and retry logic belong in separate classes composed with the adapter.
Common Pitfall 2: Leaky Abstractions
The adapter's interface should not expose adaptee-specific details. If the target interface requires adaptee-specific parameters, the abstraction has leaked.
processPayment(
stripeToken,
paypalConfig
)
processPayment(
amount,
currency
)
Gateway-specific details (tokens, credentials) should be encapsulated within the adapter, not exposed in the interface. The client should only know about domain concepts (amount, currency), not implementation details.
Common Pitfall 3: Impedance Mismatch
When the adaptee's capabilities do not align with the target interface requirements, you face impedance mismatch. For example, the target interface expects synchronous responses but the adaptee only provides asynchronous callbacks.
Solutions:
First, if the mismatch is semantic (sync vs async), consider changing the target interface to accommodate both patterns rather than forcing synchronous wrappers around async operations. Second, if the adaptee lacks required functionality (like transaction rollback), the adapter cannot magically add it. Document limitations clearly or throw UnsupportedOperationException. Third, if translation requires complex mapping logic, the adapter pattern may not be appropriate. Use a dedicated Transformer or Mapper service instead.
Testing Adapters
Unit Testing: Mock the adaptee and verify the adapter correctly translates calls. Test parameter mapping, return value conversion, and exception translation.
Integration Testing: Test adapters against real adaptee implementations (or test doubles provided by the library). Verify that your parameter translation actually works with the real API.
Contract Testing: When adapting third-party APIs, use contract tests to detect when the external API changes and breaks your adapter assumptions.
Performance Considerations
Each adapter layer adds method call overhead and potential object allocation. In performance-critical paths, measure the impact. If adapters are called millions of times per second, consider whether the abstraction is worth the cost. However, premature optimization is the root of all evil. Always profile before optimizing, but if confirmed as a bottleneck, inline the adaptation or use more direct integration for hot paths while keeping adapters for non-critical paths.