Bird
Raised Fist0
LLDsystem_design~15 mins

Adapter pattern in LLD - Deep Dive

Choose your learning style10 modes available
Overview - Adapter pattern
What is it?
The Adapter pattern is a design technique that helps two incompatible interfaces work together. It acts like a translator between two objects, allowing them to communicate even if their methods or data formats differ. This pattern is useful when you want to reuse existing code but the interfaces don't match. It creates a bridge so that one object can use another object's functionality without changing its code.
Why it matters
Without the Adapter pattern, developers would have to rewrite or heavily modify existing code to make different parts of a system work together. This wastes time and can introduce bugs. The Adapter pattern saves effort by enabling code reuse and flexibility, making systems easier to maintain and extend. It helps when integrating third-party libraries or legacy systems that don't fit neatly with new code.
Where it fits
Before learning the Adapter pattern, you should understand basic object-oriented programming concepts like classes, interfaces, and polymorphism. After mastering it, you can explore other structural design patterns like Facade and Proxy, or behavioral patterns like Strategy. It fits into the broader topic of software design patterns that improve code organization and flexibility.
Mental Model
Core Idea
The Adapter pattern wraps one object to make its interface compatible with another, enabling them to work together without changing their code.
Think of it like...
Imagine you have a phone charger with a plug that doesn't fit your wall socket. An adapter plug lets you connect your charger to the socket by converting the shape, so your phone can charge without changing the charger or the socket.
┌─────────────┐       ┌─────────────┐       ┌─────────────┐
│ Client      │──────▶│ Adapter     │──────▶│ Adaptee     │
│ (expects    │       │ (translates │       │ (existing   │
│ Target      │       │ interface)  │       │ incompatible│
│ interface)  │       │             │       │ interface)  │
└─────────────┘       └─────────────┘       └─────────────┘
Build-Up - 7 Steps
1
FoundationUnderstanding incompatible interfaces
🤔
Concept: Different classes may have methods that do similar things but with different names or parameters, making them incompatible.
Suppose you have a class that provides temperature in Celsius with a method getTemperatureCelsius(), but your client code expects a method getTemperatureFahrenheit(). Without matching methods, the client cannot use the class directly.
Result
The client cannot call the existing class's method because the names and expected data formats differ.
Understanding that interface mismatch blocks direct communication is the first step to seeing why adapters are needed.
2
FoundationRole of interfaces in communication
🤔
Concept: Interfaces define how objects communicate by specifying method names and parameters.
If two objects share the same interface, they can interact smoothly. For example, if both have a method getTemperatureFahrenheit(), the client can use either without changes.
Result
Matching interfaces enable seamless interaction between objects.
Knowing that interfaces are contracts for communication helps you see why adapting interfaces is crucial.
3
IntermediateAdapter as a translator object
🤔Before reading on: do you think the adapter changes the adaptee's code or just wraps it? Commit to your answer.
Concept: The Adapter pattern creates a new object that wraps the existing one and translates calls from the client to the adaptee's methods.
The adapter implements the target interface expected by the client. Inside, it holds a reference to the adaptee object. When the client calls a method, the adapter translates it to the adaptee's method call, possibly converting data formats.
Result
The client can use the adapter as if it were the target interface, while the adapter handles the incompatibility internally.
Understanding that the adapter wraps without modifying the adaptee preserves existing code and enables reuse.
4
IntermediateClass adapter vs object adapter
🤔Before reading on: do you think adapters always use inheritance or composition? Commit to your answer.
Concept: There are two main ways to implement adapters: by inheriting from the adaptee (class adapter) or by holding an instance of the adaptee (object adapter).
Class adapter uses multiple inheritance to inherit both the target interface and adaptee, adapting methods by overriding. Object adapter uses composition by holding an adaptee instance and forwarding calls. Object adapter is more flexible and preferred in many languages.
Result
Different adapter implementations offer trade-offs in flexibility and complexity.
Knowing these two forms helps choose the right adapter style for your language and design constraints.
5
IntermediateData conversion inside adapters
🤔Before reading on: do you think adapters only rename methods or also change data formats? Commit to your answer.
Concept: Adapters often convert data formats or units to match the client's expectations.
For example, if the adaptee returns temperature in Celsius but the client expects Fahrenheit, the adapter converts the value before returning it. This ensures the client receives data in the correct form without changing the adaptee.
Result
Adapters can handle both interface and data incompatibilities.
Understanding that adapters can transform data expands their usefulness beyond simple method renaming.
6
AdvancedAdapter pattern in large systems
🤔Before reading on: do you think adapters can be chained or combined? Commit to your answer.
Concept: In complex systems, multiple adapters can be chained to bridge several incompatible interfaces or to add layers of transformation.
For example, a client expects interface A, but the adaptee supports interface D. You can create adapters from A to B, B to C, and C to D, chaining them to achieve compatibility. This modular approach simplifies integration of diverse components.
Result
Adapters enable flexible, layered integration in large, heterogeneous systems.
Knowing adapters can be composed helps design scalable and maintainable integration solutions.
7
ExpertPerformance and design trade-offs of adapters
🤔Before reading on: do you think adapters always add negligible overhead? Commit to your answer.
Concept: Adapters add an extra layer of indirection, which can impact performance and complexity. Experts balance reuse benefits against these costs.
While adapters promote reuse and flexibility, they can introduce latency due to additional method calls and data conversions. Overusing adapters or chaining many can degrade performance. Also, adapters can hide complexity, making debugging harder. Experts carefully design adapters to minimize overhead and maintain clarity.
Result
Adapters are powerful but must be used judiciously to avoid performance and maintenance issues.
Understanding the trade-offs prevents misuse and helps design efficient, maintainable systems.
Under the Hood
The Adapter pattern works by creating a wrapper object that implements the target interface expected by the client. This wrapper holds a reference to the adaptee object. When the client calls a method on the adapter, the adapter translates this call into one or more calls on the adaptee, possibly converting parameters and return values. This translation happens at runtime, allowing the client to interact with the adaptee indirectly without knowing its interface.
Why designed this way?
The pattern was designed to enable reuse of existing code without modification, respecting the open/closed principle. Instead of changing the adaptee or client, the adapter acts as a middleman. This separation reduces risk and effort when integrating legacy or third-party code. Alternatives like rewriting code or forcing interface changes were costly and error-prone, so the adapter pattern provides a clean, modular solution.
┌─────────────┐       ┌─────────────┐       ┌─────────────┐
│ Client      │──────▶│ Adapter     │──────▶│ Adaptee     │
│ calls Target│       │ translates  │       │ original    │
│ interface  │       │ calls       │       │ interface   │
└─────────────┘       └─────────────┘       └─────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does the Adapter pattern modify the adaptee's code? Commit yes or no.
Common Belief:The adapter changes the adaptee's code to make it compatible.
Tap to reveal reality
Reality:The adapter wraps the adaptee without modifying its code, preserving its original implementation.
Why it matters:Modifying adaptee code can introduce bugs and maintenance issues; adapters avoid this by acting externally.
Quick: Is the Adapter pattern only about renaming methods? Commit yes or no.
Common Belief:Adapters only rename methods to match interfaces.
Tap to reveal reality
Reality:Adapters can also convert data formats, units, or combine multiple calls to adapt behavior.
Why it matters:Limiting adapters to renaming underestimates their power and leads to incomplete solutions.
Quick: Can adapters be chained to handle multiple incompatibilities? Commit yes or no.
Common Belief:Adapters cannot be chained; only one adapter per incompatibility is possible.
Tap to reveal reality
Reality:Adapters can be chained or layered to bridge multiple incompatible interfaces step-by-step.
Why it matters:Knowing this enables flexible integration in complex systems with many incompatible components.
Quick: Do adapters always have negligible performance impact? Commit yes or no.
Common Belief:Adapters add no meaningful overhead and can be used freely without concern.
Tap to reveal reality
Reality:Adapters add extra calls and data conversions, which can impact performance if overused or chained deeply.
Why it matters:Ignoring performance costs can lead to slow or complex systems that are hard to debug.
Expert Zone
1
Adapters can hide complexity but also obscure the real source of errors, making debugging harder.
2
Choosing between class and object adapter depends on language features like multiple inheritance support and design flexibility.
3
Adapters can be combined with other patterns like Facade or Proxy to build layered, maintainable architectures.
When NOT to use
Avoid adapters when you control both interfaces and can refactor them to match directly. Also, if performance is critical and adapter overhead is unacceptable, consider redesigning interfaces or using compile-time solutions like code generation.
Production Patterns
In real systems, adapters are used to integrate legacy databases, third-party APIs, or hardware drivers. They often appear as thin layers in microservices to translate between internal and external protocols. Adapter chaining is common in middleware stacks and plugin architectures.
Connections
Facade pattern
Both simplify interactions but Facade provides a simpler interface to a complex system, while Adapter changes an interface to match another.
Understanding Adapter clarifies how to choose between adapting interfaces versus hiding complexity with Facade.
Data format conversion
Adapter often performs data format or unit conversions as part of interface adaptation.
Knowing data conversion techniques helps implement adapters that handle more than just method renaming.
Electrical plug adapters
Adapter pattern conceptually matches physical plug adapters that enable incompatible plugs and sockets to connect.
Seeing software adapters as similar to physical adapters helps grasp their purpose and design.
Common Pitfalls
#1Modifying the adaptee's code to fit the client interface.
Wrong approach:class Adaptee { getTemperatureFahrenheit() { return this.temperatureCelsius * 9/5 + 32; } } // Changed adaptee method directly
Correct approach:class Adapter { constructor(adaptee) { this.adaptee = adaptee; } getTemperatureFahrenheit() { return this.adaptee.getTemperatureCelsius() * 9/5 + 32; } } // Adapter wraps adaptee without changing it
Root cause:Misunderstanding that adapters should preserve existing code and act externally.
#2Using adapter only to rename methods without handling data conversion.
Wrong approach:class Adapter { constructor(adaptee) { this.adaptee = adaptee; } getTemperatureFahrenheit() { return this.adaptee.getTemperatureCelsius(); } } // Returns Celsius value as Fahrenheit
Correct approach:class Adapter { constructor(adaptee) { this.adaptee = adaptee; } getTemperatureFahrenheit() { return this.adaptee.getTemperatureCelsius() * 9/5 + 32; } } // Converts Celsius to Fahrenheit correctly
Root cause:Assuming interface adaptation is only about method names, ignoring data semantics.
#3Chaining too many adapters without considering performance.
Wrong approach:AdapterA calls AdapterB calls AdapterC calls Adaptee, each adding overhead without optimization.
Correct approach:Design adapters to minimize layers or combine transformations to reduce call depth.
Root cause:Not recognizing the cumulative cost of multiple adapter layers.
Key Takeaways
The Adapter pattern enables incompatible interfaces to work together by wrapping one object with another that translates calls.
Adapters preserve existing code by acting externally, avoiding risky modifications to legacy or third-party components.
Adapters can rename methods, convert data formats, and even chain together to handle complex integration scenarios.
Choosing the right adapter style and managing performance trade-offs are key to effective use in real systems.
Understanding Adapter helps build flexible, maintainable software that can evolve without breaking existing parts.

Practice

(1/5)
1. What is the main purpose of the Adapter pattern in system design?
easy
A. To improve the performance of a system by caching results
B. To allow incompatible interfaces to work together by converting one interface to another
C. To store data persistently in a database
D. To create multiple instances of a class

Solution

  1. Step 1: Understand the Adapter pattern role

    The Adapter pattern acts as a bridge between two incompatible interfaces, allowing them to communicate.
  2. Step 2: Compare with other options

    The other options describe different design goals unrelated to interface compatibility.
  3. Final Answer:

    To allow incompatible interfaces to work together by converting one interface to another -> Option B
  4. Quick Check:

    Adapter pattern = interface compatibility [OK]
Hint: Adapter = bridge between incompatible interfaces [OK]
Common Mistakes:
  • Confusing Adapter with Singleton or Factory patterns
  • Thinking Adapter improves performance directly
  • Assuming Adapter stores data
2. Which of the following is the correct way to declare an Adapter class in a typical object-oriented language?
easy
A. class Adapter implements Adaptee { private TargetInterface target; }
B. class Adapter extends Adaptee implements TargetInterface { }
C. class Adapter extends TargetInterface { private Adaptee adaptee; }
D. class Adapter implements TargetInterface { private Adaptee adaptee; }

Solution

  1. Step 1: Identify Adapter pattern structure

    The Adapter class implements the TargetInterface and holds a reference to the Adaptee to delegate calls.
  2. Step 2: Analyze options

    class Adapter implements TargetInterface { private Adaptee adaptee; } correctly shows Adapter implementing TargetInterface and containing Adaptee. The other options misuse inheritance or interfaces.
  3. Final Answer:

    class Adapter implements TargetInterface { private Adaptee adaptee; } -> Option D
  4. Quick Check:

    Adapter implements target and wraps adaptee [OK]
Hint: Adapter implements target interface, holds adaptee instance [OK]
Common Mistakes:
  • Using inheritance incorrectly instead of composition
  • Confusing which interface to implement
  • Swapping adaptee and target roles
3. Given this Adapter pattern code snippet, what will be the output?
class Adaptee {
  String specificRequest() { return "Adaptee response"; }
}

interface Target {
  String request();
}

class Adapter implements Target {
  private Adaptee adaptee = new Adaptee();
  public String request() {
    return adaptee.specificRequest();
  }
}

public class Main {
  public static void main(String[] args) {
    Target target = new Adapter();
    System.out.println(target.request());
  }
}
medium
A. Adaptee response
B. Target response
C. Compilation error
D. Runtime exception

Solution

  1. Step 1: Trace method calls in Adapter

    The Adapter's request() calls adaptee.specificRequest(), which returns "Adaptee response".
  2. Step 2: Check main method output

    Main creates Adapter as Target and prints target.request(), so output is "Adaptee response".
  3. Final Answer:

    Adaptee response -> Option A
  4. Quick Check:

    Adapter delegates request() to adaptee.specificRequest() [OK]
Hint: Adapter.request() returns adaptee.specificRequest() result [OK]
Common Mistakes:
  • Expecting 'Target response' instead of adaptee's
  • Assuming compilation or runtime errors without cause
  • Confusing method names
4. Identify the error in this Adapter pattern implementation:
interface Target {
  void request();
}

class Adaptee {
  void specificRequest() { System.out.println("Adaptee called"); }
}

class Adapter implements Target {
  Adaptee adaptee = new Adaptee();
  public void request() {
    specificRequest();
  }
}
medium
A. Adapter calls specificRequest() without referencing adaptee instance
B. Adaptee should implement Target interface
C. Adapter should extend Adaptee instead of implementing Target
D. Target interface method signature is incorrect

Solution

  1. Step 1: Review Adapter's request() method

    Adapter calls specificRequest() directly, but specificRequest() belongs to Adaptee instance, so it should call adaptee.specificRequest().
  2. Step 2: Validate other options

    Adaptee does not need to implement Target. Adapter implements Target correctly. Target interface method is correct.
  3. Final Answer:

    Adapter calls specificRequest() without referencing adaptee instance -> Option A
  4. Quick Check:

    Adapter must call adaptee.specificRequest() [OK]
Hint: Call adaptee methods via adaptee instance inside Adapter [OK]
Common Mistakes:
  • Calling adaptee methods without instance prefix
  • Misusing inheritance instead of composition
  • Changing Target interface unnecessarily
5. You need to integrate a legacy payment system with a new e-commerce platform. The legacy system has a method processLegacyPayment(), but the platform expects processPayment(). How should you apply the Adapter pattern to solve this?
hard
A. Create a new payment system and discard the legacy system
B. Modify the legacy system to rename processLegacyPayment() to processPayment()
C. Create an Adapter class implementing the platform's payment interface and inside it call processLegacyPayment() of the legacy system
D. Use inheritance to make the legacy system extend the platform's payment interface

Solution

  1. Step 1: Understand integration requirement

    The platform expects processPayment(), but legacy system only has processLegacyPayment().
  2. Step 2: Apply Adapter pattern

    Create an Adapter that implements the platform interface and inside calls legacy's processLegacyPayment() to adapt calls without changing legacy code.
  3. Step 3: Evaluate other options

    Modifying the legacy code is risky. Discarding the legacy system is costly. Using inheritance is not possible if the legacy system is not designed for it.
  4. Final Answer:

    Create an Adapter class implementing the platform's payment interface and inside it call processLegacyPayment() of the legacy system -> Option C
  5. Quick Check:

    Adapter wraps legacy method to match platform interface [OK]
Hint: Adapter wraps legacy calls to match new interface [OK]
Common Mistakes:
  • Changing legacy code directly
  • Ignoring Adapter pattern benefits
  • Trying inheritance when not supported
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard