Abstract Factory Pattern in Java with Real-World Examples

📘 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-world projects, especially in backend systems or large-scale applications, you often need to create families of related objects — for example, different implementations for cloud services, UI components, or payment processors.

If you find yourself writing if-else or switch statements to decide which object to create, it’s time to consider the Abstract Factory Pattern.

This article explains why senior developers prefer the Abstract Factory Pattern, using real examples in Java — including a Spring Boot implementation. No theory overload. Just practical knowledge.


What Is the Abstract Factory Pattern?

The Abstract Factory Pattern is a creational design pattern that allows you to produce families of related objects without knowing their concrete classes.

Instead of writing:

if (provider.equals("aws")) {
Storage storage = new S3Storage();
Compute compute = new EC2Compute();
}

You create a factory that encapsulates this logic. Now your client code doesn’t need to know which specific storage or compute implementation it’s using.

This leads to:

  • Clean separation of concerns
  • Testable and maintainable code
  • Better support for future changes

Real-World Use Case: Cloud Provider Abstraction

Suppose you’re building a system that can run on AWS, Azure, or Google Cloud. Each provider has its own way of handling storage and compute services.

Let’s implement this using the Abstract Factory Pattern.


Step 1: Define Abstract Product Interfaces

public interface Storage {
void upload(String file);
}

public interface Compute {
void startInstance(String id);
}

Step 2: Implement Concrete Products for AWS

public class S3Storage implements Storage {
@Override
public void upload(String file) {
System.out.println("Uploading " + file + " to AWS S3");
}
}

public class EC2Compute implements Compute {
@Override
public void startInstance(String id) {
System.out.println("Starting AWS EC2 instance: " + id);
}
}

Step 3: Implement Concrete Products for Azure

public class AzureBlobStorage implements Storage {
@Override
public void upload(String file) {
System.out.println("Uploading " + file + " to Azure Blob Storage");
}
}

public class AzureVMCompute implements Compute {
@Override
public void startInstance(String id) {
System.out.println("Starting Azure VM: " + id);
}
}

Step 4: Create the Abstract Factory

public interface CloudServiceFactory {
Storage createStorage();
Compute createCompute();
}

Step 5: Create Concrete Factories for AWS and Azure

public class AWSFactory implements CloudServiceFactory {
public Storage createStorage() {
return new S3Storage();
}

public Compute createCompute() {
return new EC2Compute();
}
}

public class AzureFactory implements CloudServiceFactory {
public Storage createStorage() {
return new AzureBlobStorage();
}

public Compute createCompute() {
return new AzureVMCompute();
}
}

Step 6: Use the Factory in Client Code

public class CloudManager {
private final Storage storage;
private final Compute compute;

public CloudManager(CloudServiceFactory factory) {
this.storage = factory.createStorage();
this.compute = factory.createCompute();
}

public void deploy(String file, String instanceId) {
storage.upload(file);
compute.startInstance(instanceId);
}
}

Now let’s run it:

public class Main {
public static void main(String[] args) {
CloudServiceFactory factory = new AWSFactory(); // Or new AzureFactory();
CloudManager manager = new CloudManager(factory);

manager.deploy("report.pdf", "instance-123");
}
}

Output:

Uploading report.pdf to AWS S3  
Starting AWS EC2 instance: instance-123
This way, you can easily switch between cloud providers without touching the core business logic.

✅ Benefits in Practice

Here’s why this approach works well in real-world systems:


Spring Boot Example: Notification System

Let’s take a backend-focused scenario — sending notifications. Your app may need to send notifications via:

  • Email
  • SMS
  • Push notification

Each of them has their own logic for delivery. You also want to support adding new channels without changing existing code.

We’ll implement this in Spring Boot using Abstract Factory.


Step 1: Define the Product Interface

public interface Notifier {
void send(String to, String message);
}

Step 2: Implement Email, SMS, and Push Notifiers

@Component
public class EmailNotifier implements Notifier {
public void send(String to, String message) {
System.out.println("Email sent to " + to + ": " + message);
}
}

@Component
public class SmsNotifier implements Notifier {
public void send(String to, String message) {
System.out.println("SMS sent to " + to + ": " + message);
}
}

@Component
public class PushNotifier implements Notifier {
public void send(String to, String message) {
System.out.println("Push notification sent to " + to + ": " + message);
}
}

Step 3: Define Factory Interface

public interface NotificationFactory {
Notifier createNotifier();
}

Step 4: Create Concrete Factories

@Component("emailFactory")
public class EmailFactory implements NotificationFactory {
private final EmailNotifier emailNotifier;

public EmailFactory(EmailNotifier emailNotifier) {
this.emailNotifier = emailNotifier;
}

public Notifier createNotifier() {
return emailNotifier;
}
}

@Component("smsFactory")
public class SmsFactory implements NotificationFactory {
private final SmsNotifier smsNotifier;

public SmsFactory(SmsNotifier smsNotifier) {
this.smsNotifier = smsNotifier;
}

public Notifier createNotifier() {
return smsNotifier;
}
}

@Component("pushFactory")
public class PushFactory implements NotificationFactory {
private final PushNotifier pushNotifier;

public PushFactory(PushNotifier pushNotifier) {
this.pushNotifier = pushNotifier;
}

public Notifier createNotifier() {
return pushNotifier;
}
}

Step 5: REST Controller

@RestController
@RequestMapping("/notify")
public class NotificationController {

private final Map<String, NotificationFactory> factories;

public NotificationController(
@Qualifier("emailFactory") NotificationFactory emailFactory,
@Qualifier("smsFactory") NotificationFactory smsFactory,
@Qualifier("pushFactory") NotificationFactory pushFactory
) {
this.factories = Map.of(
"email", emailFactory,
"sms", smsFactory,
"push", pushFactory
);
}

@PostMapping("/{type}")
public ResponseEntity<String> notifyUser(
@PathVariable String type,
@RequestParam String to,
@RequestParam String message
) {
NotificationFactory factory = factories.get(type);
if (factory == null) {
return ResponseEntity.badRequest().body("Unknown notification type: " + type);
}

Notifier notifier = factory.createNotifier();
notifier.send(to, message);
return ResponseEntity.ok("Notification sent via " + type);
}
}

Example Requests

POST /notify/email?to=user@example.com&message=Hello

Output:

Email sent to user@example.com: Hello
POST /notify/sms?to=+123456789&message=Hello

Output:

SMS sent to +123456789: Hello

✅ When to Use Abstract Factory

Use it when:

  • You need to create families of related objects
  • Your code supports multiple implementations (e.g., databases, UIs, services)
  • You want to decouple object creation from usage
  • You need to make systems extensible without modifying existing code

🚫 When Not to Use It

Avoid Abstract Factory when:

  • You only have one object type to create
  • Object creation is simple and unlikely to change
  • Overhead of extra classes outweighs benefits

🏁 Conclusion

The Abstract Factory Pattern helps keep your code clean, modular, and easy to extend. It’s especially useful in:

  • Framework-level APIs
  • Multi-provider integrations
  • Configurable notification/payment systems
  • Enterprise services where object creation varies based on environment

Instead of scattering object creation logic throughout your codebase, you centralize it. That makes it easier to test, change, and scale your application — a big reason why senior developers reach for this pattern when building real systems.

If you’re working with Spring Boot or Java backends, this pattern is not just relevant — it’s practical.

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