Java Factory Pattern 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

In Java development, object creation seems simple — just call new and you’re done. But in real-world applications, object creation often depends on external factors, dynamic inputs, or configurations that can change.

That’s when using new everywhere becomes a problem. The more places you create objects manually, the harder your code becomes to maintain, test, and scale.

The Factory Pattern is a clean and reliable way to centralize object creation and make your code more flexible.

In this article, we’ll cover:

  • What the Factory Pattern is
  • What problems it solves
  • A complete Java example
  • A real-world use case (Notification system)
  • A practical Spring Boot implementation
  • When and why to use it

Let’s get started.

What Is the Factory Pattern?

The Factory Pattern is a creational design pattern. It provides a method (or a class) that decides which object to create, instead of you writing new statements throughout your code.

It gives you:

  • A single place to control how objects are made
  • A way to switch or extend object creation logic
  • Cleaner, more testable code

❌ The Problem: Too Many new Statements

Let’s say you’re building a notification system. You support multiple channels:

  • Email
  • SMS
  • Push Notification

Without a factory, you might write:

if (type.equals("EMAIL")) {
return new EmailNotification();
} else if (type.equals("SMS")) {
return new SmsNotification();
} else if (type.equals("PUSH")) {
return new PushNotification();
}

This logic might be repeated in multiple places. That leads to:

❌ Duplicated code
❌ Harder to test
❌ Violates Open/Closed Principle
❌ Scattered object creation logic

✅ The Factory Pattern Fix

With the Factory Pattern, you move all that decision-making into one place — a factory class. Now the rest of your application doesn’t care how the object is created.

You just ask for it.


🔧 Step-by-Step Java Example: Notification Factory

Let’s walk through a simple and complete example.


Step 1: Create the Interface

public interface Notification {
void send(String message);
}

Step 2: Create Implementations

public class EmailNotification implements Notification {
public void send(String message) {
System.out.println("Sending EMAIL: " + message);
}
}

public class SmsNotification implements Notification {
public void send(String message) {
System.out.println("Sending SMS: " + message);
}
}

public class PushNotification implements Notification {
public void send(String message) {
System.out.println("Sending PUSH: " + message);
}
}

Step 3: Create the Factory

public class NotificationFactory {

public static Notification create(String type) {
switch (type.toUpperCase()) {
case "EMAIL":
return new EmailNotification();
case "SMS":
return new SmsNotification();
case "PUSH":
return new PushNotification();
default:
throw new IllegalArgumentException("Unknown type: " + type);
}
}
}

Here’s the optimized Java code using the enhanced switch expression (available since Java 14+ with switch as an expression):

public class NotificationFactory {

public static Notification create(String type) {
return switch (type.toUpperCase()) {
case "EMAIL" -> new EmailNotification();
case "SMS" -> new SmsNotification();
case "PUSH" -> new PushNotification();
default -> throw new IllegalArgumentException("Unknown type: " + type);
};
}
}

The enhanced switch expression simplifies code by eliminating break statements and supporting -> syntax for clearer, more concise mapping. It also allows switch to return values directly, making the code cleaner and more maintainable.

Step 4: Use the Factory

public class Main {
public static void main(String[] args) {
Notification notification = NotificationFactory.create("email");
notification.send("Welcome to our service!");
}
}

Output:

Sending EMAIL: Welcome to our service!
✅ Easy to use
✅ No scattered
new keywords
✅ Central place to control object creation

Real-World Use Case: Payment Gateway Integration

Suppose you support multiple payment providers: Stripe, PayPal, and Razorpay.

Each provider implements a common interface:


Step 1: Define the Interface

public interface PaymentGateway {
void process(double amount);
}

Step 2: Implement Each Provider

public class StripeGateway implements PaymentGateway {
public void process(double amount) {
System.out.println("Processed $" + amount + " via Stripe");
}
}

public class PaypalGateway implements PaymentGateway {
public void process(double amount) {
System.out.println("Processed $" + amount + " via PayPal");
}
}

public class RazorpayGateway implements PaymentGateway {
public void process(double amount) {
System.out.println("Processed $" + amount + " via Razorpay");
}
}

Step 3: Create the Factory

public class PaymentGatewayFactory {

public static PaymentGateway getGateway(String type) {
switch (type.toUpperCase()) {
case "STRIPE":
return new StripeGateway();
case "PAYPAL":
return new PaypalGateway();
case "RAZORPAY":
return new RazorpayGateway();
default:
throw new IllegalArgumentException("Unknown provider: " + type);
}
}
}

Here’s the optimized version of your PaymentGatewayFactory using the Java enhanced switch expression:

public class PaymentGatewayFactory {

public static PaymentGateway getGateway(String type) {
return switch (type.toUpperCase()) {
case "STRIPE" -> new StripeGateway();
case "PAYPAL" -> new PaypalGateway();
case "RAZORPAY" -> new RazorpayGateway();
default -> throw new IllegalArgumentException("Unknown provider: " + type);
};
}
}

This version uses Java’s enhanced switch to return values directly using arrow (->) syntax, which avoids boilerplate code and improves readability. It's cleaner, more concise, and better aligns with modern Java best practices (Java 14+).

Step 4: Use in Code

public class PaymentProcessor {
public void processPayment(String provider, double amount) {
PaymentGateway gateway = PaymentGatewayFactory.getGateway(provider);
gateway.process(amount);
}
}

Now you can call:

new PaymentProcessor().processPayment("stripe", 120.0);
✅ Easy to switch providers
✅ Reusable logic
✅ Factory is testable and pluggable

💡 How It Helps in Larger Applications

Factories are especially useful when:

  • Object creation involves configuration or context
  • You want to inject dependencies at runtime
  • Your classes are used in multiple layers (controller, service, etc.)

You can go one step further and integrate it with dependency injection frameworks like Spring.


Spring Boot Example: Notification Factory with Beans

Let’s bring the concept into a Spring Boot application.


Step 1: Define the Interface and Beans

public interface NotificationService {
void send(String message);
}
@Service("email")
public class EmailService implements NotificationService {
public void send(String message) {
System.out.println("Email: " + message);
}
}

@Service("sms")
public class SmsService implements NotificationService {
public void send(String message) {
System.out.println("SMS: " + message);
}
}

Step 2: Create the Factory Using Spring

@Component
public class NotificationServiceFactory {

private final Map<String, NotificationService> services;

public NotificationServiceFactory(List<NotificationService> serviceList) {
this.services = new HashMap<>();
for (NotificationService service : serviceList) {
String key = service.getClass().getAnnotation(Service.class).value();
services.put(key.toLowerCase(), service);
}
}

public NotificationService getService(String type) {
NotificationService service = services.get(type.toLowerCase());
if (service == null) {
throw new IllegalArgumentException("Unsupported type: " + type);
}
return service;
}
}

Step 3: REST Controller

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

private final NotificationServiceFactory factory;

public NotificationController(NotificationServiceFactory factory) {
this.factory = factory;
}

@PostMapping("/{type}")
public ResponseEntity<String> notify(
@PathVariable String type,
@RequestParam String message
) {
NotificationService service = factory.getService(type);
service.send(message);
return ResponseEntity.ok("Notification sent via " + type);
}
}

Example Request

POST /notify/email?message=Hello%20World

Output:

Email: Hello World
🔥 Now adding a new service (like Push) only requires:
  1. A new @Service("push") class
  2. No changes to factory or controller

✅ Benefits of the Factory Pattern


🚫 When Not to Use It

The Factory Pattern is powerful, but not always necessary.

Avoid it when:

  • You only need to create one object and the logic is simple
  • There are no variations in creation
  • Dependency injection already solves the problem

Using a factory in a simple utility class may be overengineering.


Conclusion

The Factory Pattern is one of the most used and respected design patterns in Java development. It offers a clean way to manage object creation, making your code:

  • More maintainable
  • Easier to test
  • Flexible and extensible

Java developers love it because it fits naturally in most backend applications, especially when building APIs, services, payment systems, and integrations.

If you’re using Spring Boot, combining the Factory Pattern with dependency injection gives you even more power and cleaner architecture.

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