📘 Premium Read: Access my best content on Medium member-only articles — deep dives into Java, Spring Boot, Microservices, backend architecture, interview preparation, career advice, and industry-standard best practices.
🎓 Top 15 Udemy Courses (80-90% Discount): My Udemy Courses - Ramesh Fadatare — All my Udemy courses are real-time and project oriented courses.
▶️ Subscribe to My YouTube Channel (176K+ subscribers): Java Guides on YouTube
▶️ For AI, ChatGPT, Web, Tech, and Generative AI, subscribe to another channel: Ramesh Fadatare on YouTube
When working on real projects, we often integrate external systems — payment providers, messaging APIs, file services, etc. These systems usually have their own interfaces or SDKs, which don’t always match the way we design things internally.
If you’ve ever had to “wrap” or “translate” one interface into another, you’ve used a version of the Adapter Pattern.
In this article, we’ll walk through:
- What the Adapter Pattern is
- Why it’s useful in Spring Boot projects
- A real-world example: integrating an external SMS provider
- Step-by-step code with explanations
- When to use and avoid this pattern
Let’s get started.
What is the Adapter Pattern?
The Adapter Pattern is a structural pattern that lets you convert the interface of one class into another one your application expects.
Think of it like a plug adapter — it allows two systems with different shapes to work together.
In software, it means:
- You wrap a third-party class with a new class
- This new class implements your expected interface
- Internally, it delegates the call to the third-party method
Real-World Use Case: SMS Notification with Adapter
Scenario:
You're building a Spring Boot application. You already have a NotificationService
interface used to send messages.
Now, you want to integrate with an external SMS provider like Twilio, Nexmo, or a local vendor. But their SDK doesn’t match your internal interface.
We’ll solve this by writing an adapter that wraps the external client and makes it work with your app’s interface.
✅ Step 1: Define Your Expected Interface
This is the interface your system understands.
public interface NotificationService {
void send(String to, String message);
}
This keeps your application decoupled from specific providers.
✅ Step 2: Simulate External SMS Client
Let’s say the vendor gives you a client like this:
public class ThirdPartySmsClient {
public void sendSms(String phone, String content) {
System.out.println("Sending SMS to " + phone + ": " + content);
// Real API would hit an external service here
}
}
This class doesn’t implement your NotificationService
. So if you inject it directly, it won’t work with the rest of your code.
✅ Step 3: Create an Adapter
This is the key part.
You write a class that implements NotificationService
and wraps ThirdPartySmsClient
.
import org.springframework.stereotype.Component;
@Component("smsAdapter")
public class SmsNotificationAdapter implements NotificationService {
private final ThirdPartySmsClient smsClient;
public SmsNotificationAdapter() {
this.smsClient = new ThirdPartySmsClient(); // In real life, you'd inject config
}
@Override
public void send(String to, String message) {
smsClient.sendSms(to, message); // adapting method
}
}
Now you have a bridge between your system’s interface and the external client’s implementation.
This is the Adapter Pattern in action.
✅ Step 4: Use the Adapter in a Service
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class AlertService {
private final NotificationService notificationService;
public AlertService(@Qualifier("smsAdapter") NotificationService notificationService) {
this.notificationService = notificationService;
}
public void sendOrderAlert(String phone, String message) {
notificationService.send(phone, message);
}
}
We’re injecting our adapter as a NotificationService
. From the service’s point of view, it doesn’t matter whether the implementation is using Twilio, Nexmo, or something else.
✅ Step 5: Create a Controller to Trigger It
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/alerts")
public class AlertController {
@Autowired
private AlertService alertService;
@PostMapping
public ResponseEntity<String> sendAlert(@RequestParam String phone, @RequestParam String message) {
alertService.sendOrderAlert(phone, message);
return ResponseEntity.ok("Alert sent to " + phone);
}
}
Test with:
POST /api/alerts?phone=9876543210&message=Your+order+is+confirmed
You’ll see:
Sending SMS to 9876543210: Your order is confirmed
Why This Pattern Works Well in Spring Boot
Reason | Benefit |
---|---|
Decouples system from vendor | You can replace the vendor without touching core logic |
Easier to test | You can mock NotificationService easily in unit tests |
Makes switching easy | Want to move from local vendor to Twilio? Just write a new adapter |
Clean code | Keeps vendor logic out of your main service classes |
Testing the Adapter
Write a test using a mock version of the adapter:
@SpringBootTest
public class AlertServiceTest {
@Test
public void testSendOrderAlert() {
NotificationService mockNotification = Mockito.mock(NotificationService.class);
AlertService service = new AlertService(mockNotification);
service.sendOrderAlert("12345", "Test message");
Mockito.verify(mockNotification).send("12345", "Test message");
}
}
You didn’t test the real SMS client — and that’s okay. You tested the service logic independently.
Bonus: Add More Notification Types
Let’s say later, you add email or push notifications.
Create new adapters:
@Component("emailAdapter")
public class EmailNotificationAdapter implements NotificationService {
@Override
public void send(String to, String message) {
System.out.println("Sending Email to " + to + ": " + message);
}
}
Now you can dynamically choose adapters using a NotificationFactory
.
Factory for Selecting Notification Channel (Optional)
@Component
public class NotificationFactory {
private final Map<String, NotificationService> serviceMap;
@Autowired
public NotificationFactory(Map<String, NotificationService> serviceMap) {
this.serviceMap = serviceMap;
}
public NotificationService getService(String type) {
NotificationService service = serviceMap.get(type + "Adapter");
if (service == null) {
throw new IllegalArgumentException("Unsupported type: " + type);
}
return service;
}
}
Inject and use it:
NotificationService service = factory.getService("sms");
service.send(to, message);
✅ When to Use Adapter Pattern
Use Adapter Pattern when:
- You’re integrating third-party services or SDKs
- You want to hide vendor-specific code from your application logic
- You need to follow your own interface, not the one provided
- You plan to support multiple types of similar functionality (SMS, Email, Push)
⚠️ When NOT to Use Adapter Pattern
Avoid using it when:
- The vendor library already fits your app’s interface
- You don’t need to swap or extend implementations
- The logic is simple and not worth abstracting
Like all patterns, Adapter is useful when the complexity justifies the extra code.
Summary
Step | What We Did |
---|---|
1 | Defined our expected interface (NotificationService ) |
2 | Wrapped third-party SDK using an adapter class |
3 | Used Spring’s DI to inject the adapter |
4 | Sent notifications without caring about the actual vendor |
5 | Wrote unit tests easily by mocking the adapter |
The Adapter Pattern gives you flexibility and clean boundaries in your code — especially when working with third-party tools.
Final Thoughts
You don’t need to use design patterns everywhere. But when integrating external systems into Spring Boot apps, the Adapter Pattern is a clean and proven solution.
It helps you:
- Keep vendor code isolated
- Make testing easier
- Replace or upgrade third-party tools with zero impact on business logic
Start with simple adapters. Then, if needed, add factories to select between them dynamically.
Clean code isn’t about fewer lines — it’s about fewer surprises. The Adapter Pattern helps with that.
Comments
Post a Comment
Leave Comment