📘 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:
- 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 scatterednew
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:
- A new
@Service("push")
class - 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
Post a Comment
Leave Comment