Factory Pattern explained simply
Thilan Dissanayaka Software Architecture February 02, 2020

Factory Pattern explained simply

Factory Pattern

Imagine you want to create objects — but you don’t want to expose the creation logic to the client and instead ask a factory class to create objects for you.

That’s exactly what the Factory Pattern does.

What is the Factory Pattern?

At its core:

  • Defines an interface for creating an object.
  • Lets subclasses alter the type of objects that will be created.
  • Centralizes object creation, making code more flexible and easier to maintain.

Class Diagram

Here is how the Factory Pattern looks when applied to a notification service. The NotificationFactory decides which concrete Notification to instantiate based on a channel string, so the client never has to deal with the concrete classes directly.

Real-Life Analogy

Think about a bakery:

  • You place an order for a “cake”.
  • You don’t worry about how it’s baked.
  • The bakery (factory) prepares and gives you the cake.

You simply ask for an object and get it.

Same thing with notifications. Your application says “send this user a notification on their preferred channel” and the factory figures out whether that means firing off an email, an SMS, or a push notification. The calling code never needs to know the details.

Structure

In our notification example, here is how each piece maps to the pattern:

  • Product (Notification): The common interface. Every notification type knows how to send().
  • Concrete Products (EmailNotification, SMSNotification, PushNotification): The actual implementations that handle the channel-specific logic.
  • Creator / Factory (NotificationFactory): Has a createNotification() method that returns the right concrete type based on the channel string you pass in.

The client code only ever talks to the Notification interface. It has no idea whether it is dealing with email, SMS, or push under the hood.

Java Example – Notification Service

Let’s build something you would actually use in a real project: a notification service where users pick their preferred channel.

First, define the Notification interface:

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

Now the concrete implementations. Each one handles the channel-specific details:

public class EmailNotification implements Notification {
    @Override
    public void send(String recipient, String message) {
        System.out.println("Sending EMAIL to " + recipient + ": " + message);
        // In a real app: configure SMTP, build MIME message, hit the mail server
    }
}

public class SMSNotification implements Notification {
    @Override
    public void send(String recipient, String message) {
        System.out.println("Sending SMS to " + recipient + ": " + message);
        // In a real app: call Twilio API, handle rate limits, etc.
    }
}

public class PushNotification implements Notification {
    @Override
    public void send(String recipient, String message) {
        System.out.println("Sending PUSH to " + recipient + ": " + message);
        // In a real app: hit Firebase Cloud Messaging or APNs
    }
}

Create the NotificationFactory:

public class NotificationFactory {

    public Notification createNotification(String channel) {
        if (channel == null) {
            throw new IllegalArgumentException("Notification channel cannot be null");
        }

        switch (channel.toLowerCase()) {
            case "email":
                return new EmailNotification();
            case "sms":
                return new SMSNotification();
            case "push":
                return new PushNotification();
            default:
                throw new IllegalArgumentException("Unknown channel: " + channel);
        }
    }
}

Using the Factory

Here is where it gets practical. Imagine a user preferences system where each user has chosen how they want to be notified. The client code stays clean regardless of how many channels you support:

public class NotificationService {

    public static void main(String[] args) {
        NotificationFactory factory = new NotificationFactory();

        // Simulate user preferences loaded from a database
        String[] userChannels = {"email", "sms", "push", "email"};
        String[] recipients = {"[email protected]", "+94771234567", "device-token-xyz", "[email protected]"};

        String alertMessage = "Your account login was detected from a new device.";

        for (int i = 0; i < userChannels.length; i++) {
            Notification notification = factory.createNotification(userChannels[i]);
            notification.send(recipients[i], alertMessage);
        }
    }
}

Output:

Sending EMAIL to [email protected]: Your account login was detected from a new device.
Sending SMS to +94771234567: Your account login was detected from a new device.
Sending PUSH to device-token-xyz: Your account login was detected from a new device.
Sending EMAIL to [email protected]: Your account login was detected from a new device.

Notice how the loop does not care which notification type it is working with. It just asks the factory, gets back a Notification, and calls send(). If you add a WhatsAppNotification next month, the loop does not change at all – you only touch the factory and add the new class.

Why Use the Factory Pattern?

  • Encapsulates object creation: All the “which class do I instantiate?” logic lives in one place. When you add a new notification channel, you update the factory and nothing else.
  • Decouples code: Your notification service depends on the Notification interface, not on EmailNotification or SMSNotification directly.
  • Easier maintenance and scalability: Adding a new channel means writing one new class and one new case in the factory. Zero changes to the client code.

Real-World Use Cases

  • Notification / Messaging Systems: Exactly what we built above – routing messages to different channels based on user preferences or event types.
  • Payment Gateways: A PaymentProcessorFactory that returns a Stripe, PayPal, or Square processor depending on the merchant’s configuration.
  • Java Standard Library: Calendar.getInstance(), NumberFormat.getInstance(), DriverManager.getConnection() – all factory methods hiding the concrete implementation from you.
  • Logging Frameworks: SLF4J’s LoggerFactory.getLogger() returns the right logger implementation without you caring whether it is Logback, Log4j, or something else.
  • Cloud SDKs: Creating service clients for different cloud providers through a unified factory interface.

Factory Pattern vs. Abstract Factory Pattern

  • Factory Pattern: Creates one product type (e.g., different kinds of Notification).
  • Abstract Factory Pattern: Creates families of related products (e.g., a UIFactory that produces buttons, text fields, and dropdowns that all share the same look and feel).

Summary

The Factory Pattern helps you delegate object creation to a separate method or class. In our notification example, the calling code just says “give me a notification for this channel” and the factory handles the rest.

It hides the instantiation details and allows your code to depend on interfaces rather than concrete classes – making it more flexible, robust, and easier to extend. Next time you find yourself writing a bunch of if/else blocks to decide which class to instantiate, that is your cue to reach for a factory.

ALSO READ
Blockchain 0x000 – Understanding the Fundamentals
May 21, 2020 Web3 Development

Imagine a world where strangers can exchange money, share data, or execute agreements without ever needing to trust a central authority. No banks, no intermediaries, no single point of failure yet...

Identity and Access Management (IAM)
May 11, 2020 Identity & Access Management

Who are you — and what are you allowed to do? That's the fundamental question every secure system must answer. And it's exactly what Identity and Access Management (IAM) is built to solve.

How I built a web based CPU Simulator
May 07, 2020 Pet Projects

As someone passionate about computer engineering, reverse engineering, and system internals, I've always been fascinated by what happens "under the hood" of a computer. This curiosity led me to...

Writing a Shell Code for Linux
Apr 21, 2020 Exploit Development

Shellcode is a small piece of machine code used as the payload in exploit development. In this post, we write Linux shellcode from scratch — starting with a simple exit, building up to spawning a shell, and explaining every decision along the way.

Exploiting a Stack Buffer Overflow on Windows
Apr 12, 2020 Exploit Development

In a previous tutorial we discusses how we can exploit a buffer overflow vulnerability on a Linux machine. I wen through all theories in depth and explained each step. Now today we are going to jump...

Access Control Models
Apr 08, 2020 Identity & Access Management

Access control is one of the most fundamental concepts in security. Every time you set file permissions, assign user roles, or restrict access to a resource, you're implementing some form of access control. But not all access control is created equal...

Exploiting a  Stack Buffer Overflow  on Linux
Apr 01, 2020 Exploit Development

Have you ever wondered how attackers gain control over remote servers? How do they just run some exploit and compromise a computer? If we dive into the actual context, there is no magic happening....

Basic concepts of Cryptography
Mar 01, 2020 Cryptography

Ever notice that little padlock icon in your browser's address bar? That's cryptography working silently in the background, protecting everything you do online. Whether you're sending an email,...

Common Web Application Attacks
Feb 05, 2020 Application Security

Web applications are one of the most targeted surfaces by attackers. This is primarily because they are accessible over the internet, making them exposed and potentially vulnerable. Since these...

Remote Code Execution (RCE)
Jan 02, 2020 Application Security

Remote Code Execution (RCE) is the holy grail of application security vulnerabilities. It allows an attacker to execute arbitrary code on a remote server — and the consequences are as bad as it sounds. In this post, we'll go deep into RCE across multiple languages, including PHP, Java, Python, and Node.js.