Factory Pattern: Centralizing Object Creation for Cleaner Code

When you're building a scalable system, how you create objects matters just as much as what they do.

Let’s look at a simple requirement:

💬 “Send a notification to the user — via SMS, Email, or Push.”


A junior developer might start like this:

Notification notification = new SMSNotification();
notification.notifyUser();        

This works for now… but over time, this approach creates 5 major problems:


❌ Problems Without Factory Pattern

  1. Tight Coupling You’re directly depending on concrete classes like SMSNotification. Changing the implementation later requires touching every place where it's used.
  2. Scattered Object Creation The new keyword is scattered all over your codebase, making it hard to manage and refactor.
  3. Violation of Open/Closed Principle Adding a new notification type (e.g., Slack, WhatsApp) means changing all parts of the system that create notifications.
  4. Difficult to Unit Test Instantiating real objects in services makes mocking hard during tests.
  5. Harder to Handle Runtime Configurations What if the notification type is dynamic (e.g., read from a config file or user preference)?


✅ Solution: Factory Pattern

Let’s apply the Factory Pattern to solve all these issues step-by-step.

🧱 Step 1: Define the common interface

public interface Notification {
    void notifyUser();
}        

🧱 Step 2: Concrete implementations

public class SMSNotification implements Notification {
    public void notifyUser() {
        System.out.println("Sending SMS notification");
    }
}

public class EmailNotification implements Notification {
    public void notifyUser() {
        System.out.println("Sending Email notification");
    }
}

public class PushNotification implements Notification {
    public void notifyUser() {
        System.out.println("Sending Push notification");
    }
}        

🧱 Step 3: Create the Factory

public class NotificationFactory {

    public static Notification createNotification(String channel) {
        if (channel == null || channel.isEmpty()) {
            throw new IllegalArgumentException("Channel must not be null or empty");
        }

        switch (channel.toUpperCase()) {
            case "SMS":
                return new SMSNotification();
            case "EMAIL":
                return new EmailNotification();
            case "PUSH":
                return new PushNotification();
            default:
                throw new IllegalArgumentException("Unknown channel: " + channel);
        }
    }
}        


🧱 Step 4: Client code becomes super simple

public class NotificationService {
    public static void main(String[] args) {
        // Simulate dynamic channel source, e.g., from config or DB
        String userPreferredChannel = "EMAIL";

        Notification notification = NotificationFactory.createNotification(userPreferredChannel);
        notification.notifyUser();
    }
}        


✅ How Factory Pattern Solves Our Problems

  • Decoupled via interface
  • One central location for creation
  • Add new types by just updating factory
  • Inject mocks easily
  • Supports config/db-driven object creation


🚀 Real-World Use Case

Let’s say tomorrow the product team wants to add Slack Notification.

✅ You only need to:

  1. Create a SlackNotification class
  2. Add one line in NotificationFactory

case "SLACK":
    return new SlackNotification();        

No changes to any other part of the system. That’s powerfully scalable.


🧠 Real-Life Analogy

Imagine you’re ordering from a food delivery app.

You don’t care how food is cooked — you just select “Pizza” or “Burger” and the system delivers it.

The app is like your client code.

The kitchen (factory) decides which chef (class) prepares your order.


✅ Key Benefits Recap

  • Centralizes object creation
  • Reduces code duplication
  • Improves maintainability and scalability
  • Supports Open/Closed Principle
  • Makes unit testing easier
  • Plays well with Spring/DI frameworks


💬 Final Thought

If you're writing new all over your services, you're silently scattering responsibility and tightly coupling your code.

Use a Factory to cleanly encapsulate creation logic — your future self (and your team) will thank you.



To view or add a comment, sign in

More articles by Nikhil Bambhroliya

  • Service Locator Pattern

    🔹 Introduction In large applications, multiple components often need access to shared services (like logging…

  • Data Access Object (DAO) Pattern

    🔹 Problem Mixing SQL/database access inside your service classes quickly turns into spaghetti: Business logic tightly…

  • Null Object Pattern

    In many systems, we constantly check for before calling a method: This leads to repetitive null checks scattered across…

  • Observer Pattern

    🔹 Problem Imagine you’re building a stock market app. Investors subscribe to certain stocks.

  • Iterator Pattern

    When working with collections like lists, trees, or custom objects, we often need to traverse elements in a specific…

  • Command Pattern – Encapsulating Actions as Objects

    The Command Pattern is a behavioral design pattern that turns a request into a stand-alone object.This means you can…

  • Template Method Pattern

    🌟 Introduction The Template Method Pattern defines the skeleton of an algorithm in a base class but lets subclasses…

  • Memento Pattern

    Have you ever wished you could “undo” a mistake in your application?That’s exactly what the Memento Pattern enables —…

  • Mediator Pattern

    📝 What is the Mediator Pattern? The Mediator Pattern is a behavioral design pattern that reduces the direct…

  • Interpreter Pattern – Designing a Simple Language Interpreter

    🔥 The Problem Without Interpreter Pattern Imagine you want to build a simple language processor — like evaluating…

Others also viewed

Explore content categories