โ Scenario 1: Payment Gateway Integration using Factory Pattern
๐งฉ Problem Statement:
An application needs to support various payment modes such as Credit Card, PayPal, UPI, and Net Banking. Instantiating all services eagerly is inefficient in terms of memory and performance. The application should only instantiate the required payment service based on the user’s choice at runtime.
๐ฏ Objective:
- Create services dynamically
- Follow Open/Closed Principle
- Maintain clean separation of concerns
๐ ๏ธ Solution: Factory Pattern
The Factory Pattern is ideal when the creation of objects depends on certain conditions and you want to abstract the instantiation logic. It centralizes object creation logic and delegates it to a “factory” class.
๐งฑ Implementation Steps:
- Define an Interface:
- Create a
PaymentServiceinterface with a method likeprocessPayment().
- Create a
- Concrete Implementations:
- Implement
PaymentServicein classes likeCreditCardPaymentService,PayPalPaymentService,UPIPaymentService, andNetBankingPaymentService.
- Implement
- Create a Factory Class:
- Implement a
PaymentServiceFactoryclass with a static method likegetPaymentService(String type). - Use
switchorif-elsestatements to return the appropriate service instance.
- Implement a
- Client Interaction:
- The client layer (frontend or controller) calls the factory and invokes
processPayment()on the returned object.
- The client layer (frontend or controller) calls the factory and invokes
โ Benefits:
- Decoupling: Keeps object creation separate from business logic.
- Extensibility: Adding new payment types requires minimal changes.
- SRP Compliance: Each class has a single responsibility.
- Improved Maintainability: Easy to manage and test.
๐ Interview Follow-Up Questions:
- How is Factory different from Abstract Factory?
- When to prefer Strategy Pattern over Factory?
โ Scenario 2: Immutable Order Objects with Optional Fields using Builder Pattern
๐งฉ Problem Statement:
In many enterprise applications, especially in domain-driven design, we need immutable objects like Order, which may have many optional fields. Traditional constructors become complex and hard to maintain.
๐ฏ Objective:
- Create immutable objects with optional fields
- Avoid telescoping constructors
- Maintain object readability and clarity
๐ ๏ธ Solution: Builder Pattern
The Builder Pattern solves this problem by separating the construction of a complex object from its representation.
๐งฑ Manual Implementation:
- Immutable Order Class:
- Make all fields
final - Private constructor accepting a
Builderobject - Only
getters, nosetters
- Make all fields
- Static Inner Builder Class:
- Contains the same fields (non-final)
- Setter-like methods (
withId,withCustomerName, etc.) returnBuilderfor chaining build()method returns a new immutableOrderobject
- Client Usage:
Order order = new Order.Builder()
.withId(123)
.withCustomerName("John Doe")
.build();
โ Benefits:
- Immutability: No state changes after creation
- Readability: Code is expressive and easy to follow
- Flexibility: Only necessary fields need to be set
- No Constructor Overload: Eliminates multiple constructors
๐ง Alternative:
Use Lombok’s @Builder annotation:
- Reduces boilerplate code
- Automatically generates builder class
UserDTO.builder().username("user1").city("New York").build();
๐ Interview Follow-Up Questions:
- How does Lombok internally implement Builder?
- Can Builder ensure validation or default values?
โ Scenario 3: API Call Optimization using Proxy Pattern with Caching
๐งฉ Problem Statement:
An app interacts with a third-party payment gateway that charges per API call. Users often check transaction status repeatedly, leading to redundant and expensive calls.
๐ฏ Objective:
- Minimize third-party API calls
- Avoid repeated status checks
- Improve response time and cost efficiency
๐ ๏ธ Solution: Proxy Pattern + Caching
The Proxy Pattern introduces an intermediary that can add additional behavior (like caching) before delegating calls to the real service.
๐งฑ Implementation Steps:
- Define PaymentService Interface:
- Method:
String getTransactionStatus(String transactionId)
- Method:
- RealPaymentService:
- Connects to the real third-party API
- CachedPaymentService (Proxy):
- Implements the same interface
- Has a local cache (e.g.,
Map<String, CachedStatus>) with expiration logic - On
getTransactionStatuscall:- First check the cache
- If present and valid, return cached result
- Else, call RealPaymentService, cache result, then return
โ Benefits:
- Cost Reduction: Fewer third-party invocations
- Performance Boost: Faster response for cached data
- Avoid Rate Limits: Reduces risk of hitting third-party rate caps
- Code Transparency: Clients interact with the same interface
โ Without Caching:
- Redundant API calls
- Higher latency
- Higher costs and scalability issues
๐ Bonus Insight:
- To apply caching without modifying
RealPaymentService, use the Decorator Pattern. - Spring provides
@Cacheable, which acts like a decorator and can be applied transparently.
๐ Interview Follow-Up Questions:
- What caching strategies can be used (e.g., TTL, LRU)?
- Difference between Proxy and Decorator?
- How would you handle cache invalidation?