📘 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
If you've ever built software that had to support multiple families of related objects, you've likely run into messy if-else
or switch
code. Over time, this gets hard to test, maintain, or extend.
This is where the Abstract Factory Pattern comes in.
In this article, we’ll cover:
- What Abstract Factory Pattern is (in plain words)
- Where it's useful in real projects
- How to implement it in a Spring Boot project
- A real-world e-commerce example using payment + receipt generators
- How Spring’s dependency injection makes the pattern cleaner
No theory overload. Just simple, working code.
What Is Abstract Factory Pattern?
The Abstract Factory Pattern is a creational design pattern that allows you to create families of related objects without depending on their concrete classes.
Instead of calling new
on classes directly, you get what you need from a factory interface, which knows which specific implementation to provide.
Real-world analogy
Say you're building a checkout system for different countries. Each country has:
- A different payment gateway
- A different receipt format
You don’t want to write:
if (country.equals("US")) {
use Stripe + US receipt;
} else if (country.equals("India")) {
use Razorpay + India receipt;
}
It’s messy. It’ll only get worse as more countries are added.
Abstract Factory helps you cleanly group related behaviors — like payments and receipts — under one factory for each region.
Real Project Example: Country-Specific Payment Systems
Problem:
We want to support checkout flows in two countries: India and USA.
Each country has its own:
- Payment processor
- Receipt generator
We want to inject the right pair (payment + receipt) based on the country.
Let’s implement this step-by-step using Spring Boot.
✅ Step 1: Create Common Interfaces
public interface PaymentService {
void pay(String orderId, double amount);
}
public interface ReceiptService {
String generate(String orderId);
}
These define what all country-specific implementations must follow.
✅ Step 2: Implement for India
@Component
public class IndiaPaymentService implements PaymentService {
@Override
public void pay(String orderId, double amount) {
System.out.println("Paid ₹" + amount + " using Razorpay for Order " + orderId);
}
}
@Component
public class IndiaReceiptService implements ReceiptService {
@Override
public String generate(String orderId) {
return "Receipt [IN] for Order " + orderId;
}
}
✅ Step 3: Implement for USA
@Component
public class UsPaymentService implements PaymentService {
@Override
public void pay(String orderId, double amount) {
System.out.println("Paid $" + amount + " using Stripe for Order " + orderId);
}
}
@Component
public class UsReceiptService implements ReceiptService {
@Override
public String generate(String orderId) {
return "Receipt [US] for Order " + orderId;
}
}
✅ Step 4: Create Abstract Factory Interface
public interface CheckoutFactory {
PaymentService getPaymentService();
ReceiptService getReceiptService();
}
This is the core of the Abstract Factory Pattern. It returns a group of related objects.
✅ Step 5: Implement Factory for India
@Component("indiaFactory")
public class IndiaCheckoutFactory implements CheckoutFactory {
private final IndiaPaymentService paymentService;
private final IndiaReceiptService receiptService;
@Autowired
public IndiaCheckoutFactory(IndiaPaymentService paymentService, IndiaReceiptService receiptService) {
this.paymentService = paymentService;
this.receiptService = receiptService;
}
@Override
public PaymentService getPaymentService() {
return paymentService;
}
@Override
public ReceiptService getReceiptService() {
return receiptService;
}
}
✅ Step 6: Implement Factory for USA
@Component("usaFactory")
public class UsCheckoutFactory implements CheckoutFactory {
private final UsPaymentService paymentService;
private final UsReceiptService receiptService;
@Autowired
public UsCheckoutFactory(UsPaymentService paymentService, UsReceiptService receiptService) {
this.paymentService = paymentService;
this.receiptService = receiptService;
}
@Override
public PaymentService getPaymentService() {
return paymentService;
}
@Override
public ReceiptService getReceiptService() {
return receiptService;
}
}
Each factory now groups services for a specific country.
✅ Step 7: Create a Factory Provider (Switcher)
@Component
public class CheckoutFactoryProvider {
private final Map<String, CheckoutFactory> factoryMap;
@Autowired
public CheckoutFactoryProvider(Map<String, CheckoutFactory> factoryMap) {
this.factoryMap = factoryMap;
}
public CheckoutFactory getFactory(String countryCode) {
CheckoutFactory factory = factoryMap.get(countryCode.toLowerCase() + "Factory");
if (factory == null) {
throw new IllegalArgumentException("Unsupported country: " + countryCode);
}
return factory;
}
}
We map "indiaFactory"
and "usaFactory"
based on the component names.
✅ Step 8: OrderService to Use the Abstract Factory
@Service
public class OrderService {
private final CheckoutFactoryProvider factoryProvider;
@Autowired
public OrderService(CheckoutFactoryProvider factoryProvider) {
this.factoryProvider = factoryProvider;
}
public void checkout(String orderId, double amount, String countryCode) {
CheckoutFactory factory = factoryProvider.getFactory(countryCode);
PaymentService paymentService = factory.getPaymentService();
ReceiptService receiptService = factory.getReceiptService();
paymentService.pay(orderId, amount);
String receipt = receiptService.generate(orderId);
System.out.println("Receipt Generated: " + receipt);
}
}
✅ Step 9: REST Controller
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
@Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/checkout")
public ResponseEntity<String> checkout(@RequestParam String orderId,
@RequestParam double amount,
@RequestParam String country) {
orderService.checkout(orderId, amount, country);
return ResponseEntity.ok("Order " + orderId + " processed for country: " + country);
}
}
Test It
POST http://localhost:8080/api/orders/checkout?orderId=123&amount=1500&country=india
POST http://localhost:8080/api/orders/checkout?orderId=456&amount=50&country=usa
✅ You’ll see the correct payment and receipt services used based on the country.
Why Use Abstract Factory in Spring Boot?
Advantage | Why it matters |
---|---|
Clean structure | No giant if-else blocks for country logic |
Easy to extend | Add new countries by just creating new factory + services |
Testable | Each service and factory can be unit-tested |
Decoupled | OrderService doesn’t know about implementation details |
⚠️ When Not to Use
- If you only have one or two types, a simple
Strategy Pattern
might be enough - Don’t use Abstract Factory if the families of objects don’t change together
- Avoid over-engineering if the logic is simple and unlikely to grow
Summary
Step | What We Did |
---|---|
1 | Created common interfaces for payments and receipts |
2 | Built implementations per country |
3 | Grouped them in a country-specific factory |
4 | Used a provider to switch between factories |
5 | Called services from a central OrderService |
With Spring Boot, the Abstract Factory Pattern becomes clean and easy — thanks to component scanning and dependency injection.
Final Thoughts
In large projects, you'll often need to support multiple implementations for different business rules — country rules, partner integrations, customer types, etc.
The Abstract Factory Pattern gives you a structured way to group related logic together — keeping your services clean, testable, and easy to extend.
You don’t need to overthink design patterns — but when your if-else
or switch
logic starts spreading across layers, it’s time to refactor.
Clean factories today will save you many headaches tomorrow.
Comments
Post a Comment
Leave Comment