Abstract Factory Pattern in a Spring Boot

📘 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

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare