Hardcreational18 min read

Abstract Factory Pattern

1. What is Abstract Factory Pattern?

Definition

The Abstract Factory pattern is a GOF Creational Pattern also called Factory of Factories or Super Factory. It provides an interface for creating families of related or dependent objects without specifying their concrete classes.

One line: A factory that creates other factories, each producing a family of related products.

It is the advanced level of Factory and Factory Method patterns. While a Simple Factory creates one product type and Factory Method lets subclasses decide which product to create, Abstract Factory creates entire families of products that work together.

When does Abstract Factory apply?

In a Factory pattern, if multiple factories are creating and returning same family class objects (classes having common super class or interface), then we need the Abstract Factory pattern. It acts as a super class for the factories, providing standards to them toward object creation.

Relationship between all three factory patterns

PatternCreatesStructureAnalogy
Simple FactoryOne product type, multiple variants1 factory, N productsOne bakery making different cakes
Factory MethodOne product type, factory per variant1 abstract factory, N factory subclassesMultiple bakeries (Chennai, Noida) each making cakes their way
Abstract FactoryMultiple product families1 super factory returning N factories, each producing a familyA food court that has a bakery, a juice bar, and a snack counter

2. The Problem

Problem

When an application needs to work with multiple families of related products (e.g., JDBC vs Hibernate for database access, or Windows vs macOS for UI components), the client ends up tightly coupled to concrete implementations. Switching from one family to another requires changes throughout the application.

Problem scenario 1: DAO layer

A banking application uses JDBC for data access. The client directly creates EmployeeJdbcDAOImpl objects. When the team decides to switch to Hibernate, every service class that creates DAO objects must be changed.

Problem scenario 2: Beverage factory

A beverage company sells two product families: Fruit Juices (Mango, Orange, Mixed) and Health Drinks (Protein Shake, Energy Drink). Each has its own factory. The client must know which factory to use and how to create products from each. If a new product family is added (Smoothies), the client code must change.

Problem scenario 3: Cross-platform UI

An application needs to render UI on Windows (Windows buttons, Windows text fields, Windows checkboxes) and macOS (macOS buttons, macOS text fields, macOS checkboxes). Without Abstract Factory, the client has if/else everywhere to decide which component to create.

3. The Solution: Factory of Factories

Solution

Create an abstract factory interface/class that declares methods for creating each product in a family. Each concrete factory implements this interface and creates products for one specific family. The client works only with the abstract factory and abstract products, never knowing which concrete family it is using.

AbstractFactory (interface) +-- createProductA(): AbstractProductA +-- createProductB(): AbstractProductB / \ ConcreteFactory1 ConcreteFactory2 +-- createProductA() +-- createProductA() | returns ConcreteA1 | returns ConcreteA2 +-- createProductB() +-- createProductB() returns ConcreteB1 returns ConcreteB2 Product Families: Family 1: ConcreteA1 + ConcreteB1 (work together) Family 2: ConcreteA2 + ConcreteB2 (work together) Client knows: AbstractFactory + AbstractProductA + AbstractProductB Client does NOT know: which concrete factory or product is used

4. Example 1: Beverage Factory

From the course notes: a beverage company has two product families managed by separate factories.

Product interfaces

// FruitJuice.java
package com.nt.beverage;

public interface FruitJuice {
    void prepare();
}

// HealthDrink.java
package com.nt.beverage;

public interface HealthDrink {
    void mix();
}

Fruit Juice products

package com.nt.beverage;

public class MangoJuice implements FruitJuice {
    public void prepare() {
        System.out.println("MangoJuice :: blending mango pulp");
    }
}

public class OrangeJuice implements FruitJuice {
    public void prepare() {
        System.out.println("OrangeJuice :: squeezing fresh oranges");
    }
}

public class MixedFruitJuice implements FruitJuice {
    public void prepare() {
        System.out.println("MixedFruitJuice :: mixing all seasonal fruits");
    }
}

Health Drink products

package com.nt.beverage;

public class ProteinShake implements HealthDrink {
    public void mix() {
        System.out.println("ProteinShake :: mixing whey protein with milk");
    }
}

public class EnergyDrink implements HealthDrink {
    public void mix() {
        System.out.println("EnergyDrink :: mixing electrolytes and caffeine");
    }
}

Abstract Factory

package com.nt.factory;

public interface BeverageFactory {
    Object create(String type);
}

// Concrete factory for juices
package com.nt.factory;

import com.nt.beverage.*;

public class JuiceFactory implements BeverageFactory {
    public Object create(String type) {
        if ("mango".equalsIgnoreCase(type)) return new MangoJuice();
        if ("orange".equalsIgnoreCase(type)) return new OrangeJuice();
        if ("mixed".equalsIgnoreCase(type)) return new MixedFruitJuice();
        throw new IllegalArgumentException("Unknown juice type: " + type);
    }
}

// Concrete factory for health drinks
package com.nt.factory;

import com.nt.beverage.*;

public class HealthDrinkFactory implements BeverageFactory {
    public Object create(String type) {
        if ("protein".equalsIgnoreCase(type)) return new ProteinShake();
        if ("energy".equalsIgnoreCase(type)) return new EnergyDrink();
        throw new IllegalArgumentException("Unknown drink type: " + type);
    }
}

// Super Factory (Factory of Factories)
package com.nt.factory;

public class BeverageSuperFactory {
    public static BeverageFactory getFactory(String category) {
        if ("juice".equalsIgnoreCase(category)) return new JuiceFactory();
        if ("health".equalsIgnoreCase(category)) return new HealthDrinkFactory();
        throw new IllegalArgumentException("Unknown category: " + category);
    }
}

Client code

package com.nt.test;

import com.nt.beverage.FruitJuice;
import com.nt.beverage.HealthDrink;
import com.nt.factory.BeverageFactory;
import com.nt.factory.BeverageSuperFactory;

public class BeverageClient {
    public static void main(String[] args) {
        // Client asks super factory for a juice factory
        BeverageFactory juiceFactory = BeverageSuperFactory.getFactory("juice");
        FruitJuice mango = (FruitJuice) juiceFactory.create("mango");
        mango.prepare();

        // Client asks super factory for a health drink factory
        BeverageFactory healthFactory = BeverageSuperFactory.getFactory("health");
        HealthDrink protein = (HealthDrink) healthFactory.create("protein");
        protein.mix();
    }
}
MangoJuice :: blending mango pulp ProteinShake :: mixing whey protein with milk

5. Example 2: DAO Abstract Factory (JDBC vs Hibernate)

This is the classic enterprise example. The application can switch between JDBC and Hibernate without changing service layer code.

ApplicationFactory (factory of factories) | v DAOFactory (abstract) / \ JdbcDAOFactory HibernateDAOFactory | | | | EmployeeJdbc EmployeeHibernate DAOImpl DAOImpl | | AccountJdbc AccountHibernate DAOImpl DAOImpl
// DAO interfaces
public interface IEmployeeDAO {
    void insert(EmployeeBO bo);
    EmployeeBO find(int empno);
}

public interface IAccountDAO {
    void createAccount(AccountBO bo);
    AccountBO findByNumber(String accountNo);
}

// JDBC implementations
public class EmployeeJdbcDAOImpl implements IEmployeeDAO {
    public void insert(EmployeeBO bo) {
        System.out.println("JDBC: inserting employee " + bo.getName());
    }
    public EmployeeBO find(int empno) {
        System.out.println("JDBC: finding employee " + empno);
        return null;
    }
}

public class AccountJdbcDAOImpl implements IAccountDAO {
    public void createAccount(AccountBO bo) {
        System.out.println("JDBC: creating account " + bo.getAccountNo());
    }
    public AccountBO findByNumber(String accountNo) {
        System.out.println("JDBC: finding account " + accountNo);
        return null;
    }
}

// Hibernate implementations
public class EmployeeHibernateDAOImpl implements IEmployeeDAO {
    public void insert(EmployeeBO bo) {
        System.out.println("Hibernate: saving employee " + bo.getName());
    }
    public EmployeeBO find(int empno) {
        System.out.println("Hibernate: loading employee " + empno);
        return null;
    }
}

public class AccountHibernateDAOImpl implements IAccountDAO {
    public void createAccount(AccountBO bo) {
        System.out.println("Hibernate: persisting account " + bo.getAccountNo());
    }
    public AccountBO findByNumber(String accountNo) {
        System.out.println("Hibernate: loading account " + accountNo);
        return null;
    }
}

// Abstract Factory
public abstract class DAOFactory {
    public abstract IEmployeeDAO getEmployeeDAO();
    public abstract IAccountDAO getAccountDAO();

    public static DAOFactory getInstance(String type) {
        if ("jdbc".equalsIgnoreCase(type)) return new JdbcDAOFactory();
        if ("hibernate".equalsIgnoreCase(type)) return new HibernateDAOFactory();
        throw new IllegalArgumentException("Unknown DAO type: " + type);
    }
}

// Concrete factories
public class JdbcDAOFactory extends DAOFactory {
    public IEmployeeDAO getEmployeeDAO() { return new EmployeeJdbcDAOImpl(); }
    public IAccountDAO getAccountDAO() { return new AccountJdbcDAOImpl(); }
}

public class HibernateDAOFactory extends DAOFactory {
    public IEmployeeDAO getEmployeeDAO() { return new EmployeeHibernateDAOImpl(); }
    public IAccountDAO getAccountDAO() { return new AccountHibernateDAOImpl(); }
}

// Application Factory (super factory)
public class ApplicationFactory {
    public static DAOFactory getDAOFactory(String type) {
        return DAOFactory.getInstance(type);
    }
}

Client code

// Service layer uses only interfaces and abstract factory
DAOFactory factory = ApplicationFactory.getDAOFactory("jdbc");  // or "hibernate"
IEmployeeDAO empDAO = factory.getEmployeeDAO();
IAccountDAO accDAO = factory.getAccountDAO();

empDAO.insert(new EmployeeBO("Rajesh", 50000));
accDAO.createAccount(new AccountBO("SAV2001", "Rajesh"));
// With "jdbc": JDBC: inserting employee Rajesh JDBC: creating account SAV2001 // With "hibernate" (change ONE string): Hibernate: saving employee Rajesh Hibernate: persisting account SAV2001
Advanced: Using Properties File

For even looser coupling, read the factory type from a properties file instead of hardcoding "jdbc" or "hibernate":

# dao.properties
dao.type=hibernate

// In ApplicationFactory:
Properties props = new Properties();
props.load(getClass().getResourceAsStream("/dao.properties"));
DAOFactory factory = DAOFactory.getInstance(props.getProperty("dao.type"));

Now switching between JDBC and Hibernate requires changing only the properties file. No recompilation.

Advanced: Using Reflection

For maximum flexibility, the factory class name itself can be read from configuration and instantiated via reflection. This way, even the factory class can be swapped without modifying any Java code:

# factory.properties
dao.factory.class=com.nbi.dao.HibernateDAOFactory

// In ApplicationFactory:
String className = props.getProperty("dao.factory.class");
Class c = Class.forName(className);
DAOFactory factory = (DAOFactory) c.getDeclaredConstructor().newInstance();

This is how frameworks like Spring achieve complete decoupling between the client and the implementation.

6. Full-Length Industry Project: Cross-Platform UI Toolkit

Business Scenario

A fintech company builds a banking application that runs on both Windows and macOS. Each platform has its own look-and-feel for buttons, text fields, and checkboxes. The application must render the correct platform-specific components without hardcoding platform checks throughout the codebase. Adding a new platform (Linux) in the future should require only new component classes and a new factory, with zero changes to existing code.

High-Level Design (HLD)

+-------------------------------------------------------+ | APPLICATION LAYER | | BankingApp (main) | | | | | +-- reads platform from config | | +-- gets UIFactory from UIFactoryProvider | | +-- creates LoginScreen using factory | | +-- creates DashboardScreen using factory | +----------------------------+--------------------------+ | v +-------------------------------------------------------+ | ABSTRACT FACTORY LAYER | | | | UIFactory (interface) | | +-- createButton(text): Button | | +-- createTextField(placeholder): TextField | | +-- createCheckbox(label): Checkbox | | | | UIFactoryProvider (super factory) | | +-- getFactory(platform): UIFactory | +-------------------------------------------------------+ / \ +--------------------+ +---------------------+ | WindowsUIFactory | | MacOSUIFactory | | creates: | | creates: | | - WindowsButton | | - MacOSButton | | - WindowsTextField | | - MacOSTextField | | - WindowsCheckbox | | - MacOSCheckbox | +--------------------+ +---------------------+ | | v v +-------------------------------------------------------+ | ABSTRACT PRODUCT LAYER | | Button (interface) - render(), onClick() | | TextField (interface) - render(), getValue() | | Checkbox (interface) - render(), isChecked() | +-------------------------------------------------------+

Low-Level Design (LLD)

+---------------------------------------------+ | UIFactory (interface) | +---------------------------------------------+ | + createButton(text): Button | | + createTextField(placeholder): TextField | | + createCheckbox(label): Checkbox | +---------------------------------------------+ ^ ^ | | +-------------------+ +-------------------+ | WindowsUIFactory | | MacOSUIFactory | +-------------------+ +-------------------+ +---------------------------------------------+ | Button (interface) | +---------------------------------------------+ | + render(): void | | + onClick(action: Runnable): void | | + getText(): String | +---------------------------------------------+ ^ ^ | | +----------------+ +----------------+ | WindowsButton | | MacOSButton | | style: [====] | | style: (====) | +----------------+ +----------------+ +---------------------------------------------+ | TextField (interface) | +---------------------------------------------+ | + render(): void | | + setValue(text): void | | + getValue(): String | +---------------------------------------------+ ^ ^ | | +------------------+ +------------------+ | WindowsTextField | | MacOSTextField | | style: |_____| | | style: [_____| | +------------------+ +------------------+ +---------------------------------------------+ | Checkbox (interface) | +---------------------------------------------+ | + render(): void | | + toggle(): void | | + isChecked(): boolean | +---------------------------------------------+ ^ ^ | | +------------------+ +------------------+ | WindowsCheckbox | | MacOSCheckbox | | style: [x] | | style: (x) | +------------------+ +------------------+ +---------------------------------------------+ | UIFactoryProvider (super factory) | +---------------------------------------------+ | + getFactory(platform): UIFactory [static] | | Reads platform, returns correct factory. | | Adding Linux = add LinuxUIFactory + products| | No changes to existing code. | +---------------------------------------------+ +---------------------------------------------+ | Screen classes (use factory) | +---------------------------------------------+ | LoginScreen | | - factory: UIFactory | | - usernameField, passwordField, loginButton | | + render(): void | +---------------------------------------------+ | DashboardScreen | | - factory: UIFactory | | - welcomeButton, settingsCheckbox | | + render(): void | +---------------------------------------------+

Project Structure

cross-platform-ui-demo/ ├── src/ │ └── main/java/com/nbi/ui/ │ ├── product/ │ │ ├── Button.java (interface) │ │ ├── TextField.java (interface) │ │ ├── Checkbox.java (interface) │ │ ├── windows/ │ │ │ ├── WindowsButton.java │ │ │ ├── WindowsTextField.java │ │ │ └── WindowsCheckbox.java │ │ └── macos/ │ │ ├── MacOSButton.java │ │ ├── MacOSTextField.java │ │ └── MacOSCheckbox.java │ ├── factory/ │ │ ├── UIFactory.java (abstract factory interface) │ │ ├── WindowsUIFactory.java │ │ ├── MacOSUIFactory.java │ │ └── UIFactoryProvider.java (super factory) │ ├── screen/ │ │ ├── LoginScreen.java │ │ └── DashboardScreen.java │ └── Application.java ├── src/ │ └── test/java/com/nbi/ui/ │ ├── factory/ │ │ └── UIFactoryProviderTest.java │ └── screen/ │ └── LoginScreenTest.java ├── src/ │ └── main/resources/ │ └── ui.properties └── pom.xml

ui.properties

# Change this to switch the entire UI family
# Supported: windows, macos
ui.platform=windows

Product interfaces

// Button.java
package com.nbi.ui.product;

public interface Button {
    void render();
    void onClick(Runnable action);
    String getText();
}

// TextField.java
package com.nbi.ui.product;

public interface TextField {
    void render();
    void setValue(String text);
    String getValue();
}

// Checkbox.java
package com.nbi.ui.product;

public interface Checkbox {
    void render();
    void toggle();
    boolean isChecked();
}

Windows products

// WindowsButton.java
package com.nbi.ui.product.windows;

import com.nbi.ui.product.Button;

public class WindowsButton implements Button {
    private final String text;

    public WindowsButton(String text) {
        this.text = text;
    }

    @Override
    public void render() {
        System.out.println("  [Windows Button] [ " + text + " ]");
    }

    @Override
    public void onClick(Runnable action) {
        System.out.println("  [Windows Button] clicked: " + text);
        action.run();
    }

    @Override
    public String getText() { return text; }
}

// WindowsTextField.java
package com.nbi.ui.product.windows;

import com.nbi.ui.product.TextField;

public class WindowsTextField implements TextField {
    private final String placeholder;
    private String value = "";

    public WindowsTextField(String placeholder) {
        this.placeholder = placeholder;
    }

    @Override
    public void render() {
        String display = value.isEmpty() ? placeholder : value;
        System.out.println("  [Windows TextField] |" + display + "|");
    }

    @Override
    public void setValue(String text) { this.value = text; }

    @Override
    public String getValue() { return value; }
}

// WindowsCheckbox.java
package com.nbi.ui.product.windows;

import com.nbi.ui.product.Checkbox;

public class WindowsCheckbox implements Checkbox {
    private final String label;
    private boolean checked = false;

    public WindowsCheckbox(String label) {
        this.label = label;
    }

    @Override
    public void render() {
        String mark = checked ? "x" : " ";
        System.out.println("  [Windows Checkbox] [" + mark + "] " + label);
    }

    @Override
    public void toggle() { checked = !checked; }

    @Override
    public boolean isChecked() { return checked; }
}

macOS products

// MacOSButton.java
package com.nbi.ui.product.macos;

import com.nbi.ui.product.Button;

public class MacOSButton implements Button {
    private final String text;

    public MacOSButton(String text) {
        this.text = text;
    }

    @Override
    public void render() {
        System.out.println("  (macOS Button) ( " + text + " )");
    }

    @Override
    public void onClick(Runnable action) {
        System.out.println("  (macOS Button) tapped: " + text);
        action.run();
    }

    @Override
    public String getText() { return text; }
}

// MacOSTextField.java
package com.nbi.ui.product.macos;

import com.nbi.ui.product.TextField;

public class MacOSTextField implements TextField {
    private final String placeholder;
    private String value = "";

    public MacOSTextField(String placeholder) {
        this.placeholder = placeholder;
    }

    @Override
    public void render() {
        String display = value.isEmpty() ? placeholder : value;
        System.out.println("  (macOS TextField) [" + display + "]");
    }

    @Override
    public void setValue(String text) { this.value = text; }

    @Override
    public String getValue() { return value; }
}

// MacOSCheckbox.java
package com.nbi.ui.product.macos;

import com.nbi.ui.product.Checkbox;

public class MacOSCheckbox implements Checkbox {
    private final String label;
    private boolean checked = false;

    public MacOSCheckbox(String label) {
        this.label = label;
    }

    @Override
    public void render() {
        String mark = checked ? "x" : " ";
        System.out.println("  (macOS Checkbox) (" + mark + ") " + label);
    }

    @Override
    public void toggle() { checked = !checked; }

    @Override
    public boolean isChecked() { return checked; }
}

UIFactory.java (Abstract Factory interface)

package com.nbi.ui.factory;

import com.nbi.ui.product.Button;
import com.nbi.ui.product.TextField;
import com.nbi.ui.product.Checkbox;

public interface UIFactory {
    Button createButton(String text);
    TextField createTextField(String placeholder);
    Checkbox createCheckbox(String label);
    String getPlatformName();
}

WindowsUIFactory.java

package com.nbi.ui.factory;

import com.nbi.ui.product.Button;
import com.nbi.ui.product.TextField;
import com.nbi.ui.product.Checkbox;
import com.nbi.ui.product.windows.WindowsButton;
import com.nbi.ui.product.windows.WindowsTextField;
import com.nbi.ui.product.windows.WindowsCheckbox;

public class WindowsUIFactory implements UIFactory {
    @Override
    public Button createButton(String text) { return new WindowsButton(text); }

    @Override
    public TextField createTextField(String placeholder) { return new WindowsTextField(placeholder); }

    @Override
    public Checkbox createCheckbox(String label) { return new WindowsCheckbox(label); }

    @Override
    public String getPlatformName() { return "Windows 11"; }
}

MacOSUIFactory.java

package com.nbi.ui.factory;

import com.nbi.ui.product.Button;
import com.nbi.ui.product.TextField;
import com.nbi.ui.product.Checkbox;
import com.nbi.ui.product.macos.MacOSButton;
import com.nbi.ui.product.macos.MacOSTextField;
import com.nbi.ui.product.macos.MacOSCheckbox;

public class MacOSUIFactory implements UIFactory {
    @Override
    public Button createButton(String text) { return new MacOSButton(text); }

    @Override
    public TextField createTextField(String placeholder) { return new MacOSTextField(placeholder); }

    @Override
    public Checkbox createCheckbox(String label) { return new MacOSCheckbox(label); }

    @Override
    public String getPlatformName() { return "macOS Sonoma"; }
}

UIFactoryProvider.java (Super Factory)

package com.nbi.ui.factory;

public class UIFactoryProvider {

    public static UIFactory getFactory(String platform) {
        if (platform == null) {
            throw new IllegalArgumentException("Platform cannot be null");
        }

        switch (platform.toLowerCase()) {
            case "windows":
                return new WindowsUIFactory();
            case "macos":
            case "mac":
                return new MacOSUIFactory();
            default:
                throw new IllegalArgumentException(
                    "Unsupported platform: " + platform
                    + ". Supported: windows, macos");
        }
    }
}

LoginScreen.java

package com.nbi.ui.screen;

import com.nbi.ui.factory.UIFactory;
import com.nbi.ui.product.Button;
import com.nbi.ui.product.TextField;
import com.nbi.ui.product.Checkbox;

public class LoginScreen {
    private final UIFactory factory;
    private final TextField usernameField;
    private final TextField passwordField;
    private final Checkbox rememberMe;
    private final Button loginButton;

    public LoginScreen(UIFactory factory) {
        this.factory = factory;
        this.usernameField = factory.createTextField("Enter username");
        this.passwordField = factory.createTextField("Enter password");
        this.rememberMe = factory.createCheckbox("Remember me");
        this.loginButton = factory.createButton("Login");
    }

    public void render() {
        System.out.println("--- Login Screen (" + factory.getPlatformName() + ") ---");
        usernameField.render();
        passwordField.render();
        rememberMe.render();
        loginButton.render();
    }

    public void simulateLogin(String username, String password) {
        usernameField.setValue(username);
        passwordField.setValue(password);
        rememberMe.toggle();

        System.out.println("\nAfter user input:");
        usernameField.render();
        passwordField.render();
        rememberMe.render();

        loginButton.onClick(() ->
            System.out.println("  Logging in as: " + usernameField.getValue()));
    }
}

DashboardScreen.java

package com.nbi.ui.screen;

import com.nbi.ui.factory.UIFactory;
import com.nbi.ui.product.Button;
import com.nbi.ui.product.Checkbox;

public class DashboardScreen {
    private final UIFactory factory;
    private final Button transferButton;
    private final Button balanceButton;
    private final Checkbox notificationsCheckbox;

    public DashboardScreen(UIFactory factory) {
        this.factory = factory;
        this.transferButton = factory.createButton("Transfer Money");
        this.balanceButton = factory.createButton("Check Balance");
        this.notificationsCheckbox = factory.createCheckbox("Enable notifications");
    }

    public void render() {
        System.out.println("--- Dashboard (" + factory.getPlatformName() + ") ---");
        transferButton.render();
        balanceButton.render();
        notificationsCheckbox.render();
    }
}

Application.java

package com.nbi.ui;

import com.nbi.ui.factory.UIFactory;
import com.nbi.ui.factory.UIFactoryProvider;
import com.nbi.ui.screen.LoginScreen;
import com.nbi.ui.screen.DashboardScreen;
import java.io.InputStream;
import java.util.Properties;

public class Application {
    public static void main(String[] args) {
        // Read platform from config
        String platform = readPlatform();
        System.out.println("Platform: " + platform);
        System.out.println();

        // Get factory for the platform
        UIFactory factory = UIFactoryProvider.getFactory(platform);

        // Scenario 1: Render Login Screen
        LoginScreen loginScreen = new LoginScreen(factory);
        loginScreen.render();
        System.out.println();

        // Scenario 2: Simulate user login
        loginScreen.simulateLogin("rajesh.kumar", "nbi@2026");
        System.out.println();

        // Scenario 3: Render Dashboard
        DashboardScreen dashboard = new DashboardScreen(factory);
        dashboard.render();
        System.out.println();

        // Scenario 4: Switch platform at runtime
        System.out.println("=== Switching to macOS ===");
        UIFactory macFactory = UIFactoryProvider.getFactory("macos");
        LoginScreen macLogin = new LoginScreen(macFactory);
        macLogin.render();
        System.out.println();

        DashboardScreen macDash = new DashboardScreen(macFactory);
        macDash.render();
    }

    private static String readPlatform() {
        try (InputStream is = Application.class.getClassLoader()
                .getResourceAsStream("ui.properties")) {
            if (is != null) {
                Properties props = new Properties();
                props.load(is);
                return props.getProperty("ui.platform", "windows");
            }
        } catch (Exception e) {
            System.out.println("Could not read config, defaulting to windows");
        }
        return "windows";
    }
}

Expected Output

Platform: windows --- Login Screen (Windows 11) --- [Windows TextField] |Enter username| [Windows TextField] |Enter password| [Windows Checkbox] [ ] Remember me [Windows Button] [ Login ] After user input: [Windows TextField] |rajesh.kumar| [Windows TextField] |nbi@2026| [Windows Checkbox] [x] Remember me [Windows Button] clicked: Login Logging in as: rajesh.kumar --- Dashboard (Windows 11) --- [Windows Button] [ Transfer Money ] [Windows Button] [ Check Balance ] [Windows Checkbox] [ ] Enable notifications === Switching to macOS === --- Login Screen (macOS Sonoma) --- (macOS TextField) [Enter username] (macOS TextField) [Enter password] (macOS Checkbox) ( ) Remember me (macOS Button) ( Login ) --- Dashboard (macOS Sonoma) --- (macOS Button) ( Transfer Money ) (macOS Button) ( Check Balance ) (macOS Checkbox) ( ) Enable notifications

UIFactoryProviderTest.java

package com.nbi.ui.factory;

import com.nbi.ui.product.Button;
import com.nbi.ui.product.TextField;
import com.nbi.ui.product.Checkbox;
import com.nbi.ui.product.windows.WindowsButton;
import com.nbi.ui.product.macos.MacOSButton;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class UIFactoryProviderTest {

    @Test
    void getFactory_returnsWindowsFactory() {
        UIFactory factory = UIFactoryProvider.getFactory("windows");
        Button btn = factory.createButton("Test");
        assertTrue(btn instanceof WindowsButton);
    }

    @Test
    void getFactory_returnsMacOSFactory() {
        UIFactory factory = UIFactoryProvider.getFactory("macos");
        Button btn = factory.createButton("Test");
        assertTrue(btn instanceof MacOSButton);
    }

    @Test
    void getFactory_isCaseInsensitive() {
        UIFactory f1 = UIFactoryProvider.getFactory("WINDOWS");
        UIFactory f2 = UIFactoryProvider.getFactory("windows");
        assertEquals(f1.getPlatformName(), f2.getPlatformName());
    }

    @Test
    void getFactory_throwsForNull() {
        assertThrows(IllegalArgumentException.class,
            () -> UIFactoryProvider.getFactory(null));
    }

    @Test
    void getFactory_throwsForUnsupported() {
        assertThrows(IllegalArgumentException.class,
            () -> UIFactoryProvider.getFactory("linux"));
    }

    @Test
    void windowsFactory_createsConsistentFamily() {
        UIFactory factory = UIFactoryProvider.getFactory("windows");
        assertNotNull(factory.createButton("btn"));
        assertNotNull(factory.createTextField("field"));
        assertNotNull(factory.createCheckbox("check"));
    }

    @Test
    void macFactory_createsConsistentFamily() {
        UIFactory factory = UIFactoryProvider.getFactory("macos");
        assertNotNull(factory.createButton("btn"));
        assertNotNull(factory.createTextField("field"));
        assertNotNull(factory.createCheckbox("check"));
    }

    @Test
    void productsFromSameFactory_areConsistent() {
        UIFactory winFactory = UIFactoryProvider.getFactory("windows");
        UIFactory macFactory = UIFactoryProvider.getFactory("macos");

        // Windows products should be Windows type
        assertTrue(winFactory.createButton("x") instanceof WindowsButton);
        // macOS products should be macOS type
        assertTrue(macFactory.createButton("x") instanceof MacOSButton);
    }
}

Key observations from the project

  1. LoginScreen and DashboardScreen never import platform-specific classes. They work only with UIFactory, Button, TextField, Checkbox interfaces.
  2. Switching platforms requires changing one config value (ui.platform=macos). Zero code changes.
  3. Adding Linux support: Create LinuxButton, LinuxTextField, LinuxCheckbox, LinuxUIFactory, and add one case to UIFactoryProvider. No existing file is modified.
  4. Products within a family are consistent. WindowsUIFactory only creates Windows components. You cannot accidentally mix a WindowsButton with a MacOSTextField.
  5. The super factory hides all concrete factories. The client calls UIFactoryProvider.getFactory("windows") and gets back a UIFactory interface. It never knows about WindowsUIFactory.

7. When to Use and When NOT to Use

When to use Abstract Factory

ScenarioExampleWhy Abstract Factory
Multiple product familiesJDBC vs Hibernate DAO implementationsEach family has multiple related products that must work together
Cross-platform UIWindows vs macOS vs Linux componentsButtons, fields, checkboxes must all be from the same platform
Theme systemsLight theme vs Dark theme componentsAll themed components must match
Multi-vendor integrationAWS vs Azure cloud servicesStorage, compute, and messaging must all be from the same vendor
Test vs ProductionReal services vs Mock servicesAll dependencies must be consistently real or mock

When NOT to use Abstract Factory

ScenarioWhy NOTUse Instead
Single product typeOnly one product, not a familySimple Factory or Factory Method
Products are independentButton and database connection are unrelatedSeparate factories
Only one family existsNo need for abstraction over one implementationDirect instantiation
Simple applicationAbstract Factory adds many classes for simple casesSimple Factory

8. Quick Reference Card

AspectSimple FactoryFactory MethodAbstract Factory
GOF Pattern?No (informal)YesYes
CreatesOne product typeOne product typeFamily of products
Factories1 factory classN factory subclassesN family factories
ExtensionModify factory (violates OCP)Add factory subclassAdd entire factory family
ConsistencyNot guaranteedPer-factoryEntire family consistent
ComplexityLowMediumHigh
JDK exampleCalendar.getInstance()Collection.iterator()javax.xml.parsers.DocumentBuilderFactory
Creational GOF Pattern Hard

Abstract Factory Design Pattern in Java

Complete Tutorial | 12 Sections | Full Cross-Platform UI Project with HLD and LLD | Factory of Factories | DAO and Beverage Examples | Multiple Scenarios | 12 Interview Questions

1. What is Abstract Factory Pattern?

Definition

The Abstract Factory pattern is a GOF Creational Pattern also called Factory of Factories or Super Factory. It provides an interface for creating families of related or dependent objects without specifying their concrete classes.

One line: A factory that creates other factories, each producing a family of related products.

It is the advanced level of Factory and Factory Method patterns. While a Simple Factory creates one product type and Factory Method lets subclasses decide which product to create, Abstract Factory creates entire families of products that work together.

When does Abstract Factory apply?

In a Factory pattern, if multiple factories are creating and returning same family class objects (classes having common super class or interface), then we need the Abstract Factory pattern. It acts as a super class for the factories, providing standards to them toward object creation.

Relationship between all three factory patterns

PatternCreatesStructureAnalogy
Simple FactoryOne product type, multiple variants1 factory, N productsOne bakery making different cakes
Factory MethodOne product type, factory per variant1 abstract factory, N factory subclassesMultiple bakeries (Chennai, Noida) each making cakes their way
Abstract FactoryMultiple product families1 super factory returning N factories, each producing a familyA food court that has a bakery, a juice bar, and a snack counter

2. The Problem

Problem

When an application needs to work with multiple families of related products (e.g., JDBC vs Hibernate for database access, or Windows vs macOS for UI components), the client ends up tightly coupled to concrete implementations. Switching from one family to another requires changes throughout the application.

Problem scenario 1: DAO layer

A banking application uses JDBC for data access. The client directly creates EmployeeJdbcDAOImpl objects. When the team decides to switch to Hibernate, every service class that creates DAO objects must be changed.

Problem scenario 2: Beverage factory

A beverage company sells two product families: Fruit Juices (Mango, Orange, Mixed) and Health Drinks (Protein Shake, Energy Drink). Each has its own factory. The client must know which factory to use and how to create products from each. If a new product family is added (Smoothies), the client code must change.

Problem scenario 3: Cross-platform UI

An application needs to render UI on Windows (Windows buttons, Windows text fields, Windows checkboxes) and macOS (macOS buttons, macOS text fields, macOS checkboxes). Without Abstract Factory, the client has if/else everywhere to decide which component to create.

3. The Solution: Factory of Factories

Solution

Create an abstract factory interface/class that declares methods for creating each product in a family. Each concrete factory implements this interface and creates products for one specific family. The client works only with the abstract factory and abstract products, never knowing which concrete family it is using.

AbstractFactory (interface) +-- createProductA(): AbstractProductA +-- createProductB(): AbstractProductB / \ ConcreteFactory1 ConcreteFactory2 +-- createProductA() +-- createProductA() | returns ConcreteA1 | returns ConcreteA2 +-- createProductB() +-- createProductB() returns ConcreteB1 returns ConcreteB2 Product Families: Family 1: ConcreteA1 + ConcreteB1 (work together) Family 2: ConcreteA2 + ConcreteB2 (work together) Client knows: AbstractFactory + AbstractProductA + AbstractProductB Client does NOT know: which concrete factory or product is used

4. Example 1: Beverage Factory

From the course notes: a beverage company has two product families managed by separate factories.

Product interfaces

// FruitJuice.java
package com.nt.beverage;

public interface FruitJuice {
    void prepare();
}

// HealthDrink.java
package com.nt.beverage;

public interface HealthDrink {
    void mix();
}

Fruit Juice products

package com.nt.beverage;

public class MangoJuice implements FruitJuice {
    public void prepare() {
        System.out.println("MangoJuice :: blending mango pulp");
    }
}

public class OrangeJuice implements FruitJuice {
    public void prepare() {
        System.out.println("OrangeJuice :: squeezing fresh oranges");
    }
}

public class MixedFruitJuice implements FruitJuice {
    public void prepare() {
        System.out.println("MixedFruitJuice :: mixing all seasonal fruits");
    }
}

Health Drink products

package com.nt.beverage;

public class ProteinShake implements HealthDrink {
    public void mix() {
        System.out.println("ProteinShake :: mixing whey protein with milk");
    }
}

public class EnergyDrink implements HealthDrink {
    public void mix() {
        System.out.println("EnergyDrink :: mixing electrolytes and caffeine");
    }
}

Abstract Factory

package com.nt.factory;

public interface BeverageFactory {
    Object create(String type);
}

// Concrete factory for juices
package com.nt.factory;

import com.nt.beverage.*;

public class JuiceFactory implements BeverageFactory {
    public Object create(String type) {
        if ("mango".equalsIgnoreCase(type)) return new MangoJuice();
        if ("orange".equalsIgnoreCase(type)) return new OrangeJuice();
        if ("mixed".equalsIgnoreCase(type)) return new MixedFruitJuice();
        throw new IllegalArgumentException("Unknown juice type: " + type);
    }
}

// Concrete factory for health drinks
package com.nt.factory;

import com.nt.beverage.*;

public class HealthDrinkFactory implements BeverageFactory {
    public Object create(String type) {
        if ("protein".equalsIgnoreCase(type)) return new ProteinShake();
        if ("energy".equalsIgnoreCase(type)) return new EnergyDrink();
        throw new IllegalArgumentException("Unknown drink type: " + type);
    }
}

// Super Factory (Factory of Factories)
package com.nt.factory;

public class BeverageSuperFactory {
    public static BeverageFactory getFactory(String category) {
        if ("juice".equalsIgnoreCase(category)) return new JuiceFactory();
        if ("health".equalsIgnoreCase(category)) return new HealthDrinkFactory();
        throw new IllegalArgumentException("Unknown category: " + category);
    }
}

Client code

package com.nt.test;

import com.nt.beverage.FruitJuice;
import com.nt.beverage.HealthDrink;
import com.nt.factory.BeverageFactory;
import com.nt.factory.BeverageSuperFactory;

public class BeverageClient {
    public static void main(String[] args) {
        // Client asks super factory for a juice factory
        BeverageFactory juiceFactory = BeverageSuperFactory.getFactory("juice");
        FruitJuice mango = (FruitJuice) juiceFactory.create("mango");
        mango.prepare();

        // Client asks super factory for a health drink factory
        BeverageFactory healthFactory = BeverageSuperFactory.getFactory("health");
        HealthDrink protein = (HealthDrink) healthFactory.create("protein");
        protein.mix();
    }
}
MangoJuice :: blending mango pulp ProteinShake :: mixing whey protein with milk

5. Example 2: DAO Abstract Factory (JDBC vs Hibernate)

This is the classic enterprise example. The application can switch between JDBC and Hibernate without changing service layer code.

ApplicationFactory (factory of factories) | v DAOFactory (abstract) / \ JdbcDAOFactory HibernateDAOFactory | | | | EmployeeJdbc EmployeeHibernate DAOImpl DAOImpl | | AccountJdbc AccountHibernate DAOImpl DAOImpl
// DAO interfaces
public interface IEmployeeDAO {
    void insert(EmployeeBO bo);
    EmployeeBO find(int empno);
}

public interface IAccountDAO {
    void createAccount(AccountBO bo);
    AccountBO findByNumber(String accountNo);
}

// JDBC implementations
public class EmployeeJdbcDAOImpl implements IEmployeeDAO {
    public void insert(EmployeeBO bo) {
        System.out.println("JDBC: inserting employee " + bo.getName());
    }
    public EmployeeBO find(int empno) {
        System.out.println("JDBC: finding employee " + empno);
        return null;
    }
}

public class AccountJdbcDAOImpl implements IAccountDAO {
    public void createAccount(AccountBO bo) {
        System.out.println("JDBC: creating account " + bo.getAccountNo());
    }
    public AccountBO findByNumber(String accountNo) {
        System.out.println("JDBC: finding account " + accountNo);
        return null;
    }
}

// Hibernate implementations
public class EmployeeHibernateDAOImpl implements IEmployeeDAO {
    public void insert(EmployeeBO bo) {
        System.out.println("Hibernate: saving employee " + bo.getName());
    }
    public EmployeeBO find(int empno) {
        System.out.println("Hibernate: loading employee " + empno);
        return null;
    }
}

public class AccountHibernateDAOImpl implements IAccountDAO {
    public void createAccount(AccountBO bo) {
        System.out.println("Hibernate: persisting account " + bo.getAccountNo());
    }
    public AccountBO findByNumber(String accountNo) {
        System.out.println("Hibernate: loading account " + accountNo);
        return null;
    }
}

// Abstract Factory
public abstract class DAOFactory {
    public abstract IEmployeeDAO getEmployeeDAO();
    public abstract IAccountDAO getAccountDAO();

    public static DAOFactory getInstance(String type) {
        if ("jdbc".equalsIgnoreCase(type)) return new JdbcDAOFactory();
        if ("hibernate".equalsIgnoreCase(type)) return new HibernateDAOFactory();
        throw new IllegalArgumentException("Unknown DAO type: " + type);
    }
}

// Concrete factories
public class JdbcDAOFactory extends DAOFactory {
    public IEmployeeDAO getEmployeeDAO() { return new EmployeeJdbcDAOImpl(); }
    public IAccountDAO getAccountDAO() { return new AccountJdbcDAOImpl(); }
}

public class HibernateDAOFactory extends DAOFactory {
    public IEmployeeDAO getEmployeeDAO() { return new EmployeeHibernateDAOImpl(); }
    public IAccountDAO getAccountDAO() { return new AccountHibernateDAOImpl(); }
}

// Application Factory (super factory)
public class ApplicationFactory {
    public static DAOFactory getDAOFactory(String type) {
        return DAOFactory.getInstance(type);
    }
}

Client code

// Service layer uses only interfaces and abstract factory
DAOFactory factory = ApplicationFactory.getDAOFactory("jdbc");  // or "hibernate"
IEmployeeDAO empDAO = factory.getEmployeeDAO();
IAccountDAO accDAO = factory.getAccountDAO();

empDAO.insert(new EmployeeBO("Rajesh", 50000));
accDAO.createAccount(new AccountBO("SAV2001", "Rajesh"));
// With "jdbc": JDBC: inserting employee Rajesh JDBC: creating account SAV2001 // With "hibernate" (change ONE string): Hibernate: saving employee Rajesh Hibernate: persisting account SAV2001
Advanced: Using Properties File

For even looser coupling, read the factory type from a properties file instead of hardcoding "jdbc" or "hibernate":

# dao.properties
dao.type=hibernate

// In ApplicationFactory:
Properties props = new Properties();
props.load(getClass().getResourceAsStream("/dao.properties"));
DAOFactory factory = DAOFactory.getInstance(props.getProperty("dao.type"));

Now switching between JDBC and Hibernate requires changing only the properties file. No recompilation.

Advanced: Using Reflection

For maximum flexibility, the factory class name itself can be read from configuration and instantiated via reflection. This way, even the factory class can be swapped without modifying any Java code:

# factory.properties
dao.factory.class=com.nbi.dao.HibernateDAOFactory

// In ApplicationFactory:
String className = props.getProperty("dao.factory.class");
Class c = Class.forName(className);
DAOFactory factory = (DAOFactory) c.getDeclaredConstructor().newInstance();

This is how frameworks like Spring achieve complete decoupling between the client and the implementation.

6. Full-Length Industry Project: Cross-Platform UI Toolkit

Business Scenario

A fintech company builds a banking application that runs on both Windows and macOS. Each platform has its own look-and-feel for buttons, text fields, and checkboxes. The application must render the correct platform-specific components without hardcoding platform checks throughout the codebase. Adding a new platform (Linux) in the future should require only new component classes and a new factory, with zero changes to existing code.

High-Level Design (HLD)

+-------------------------------------------------------+ | APPLICATION LAYER | | BankingApp (main) | | | | | +-- reads platform from config | | +-- gets UIFactory from UIFactoryProvider | | +-- creates LoginScreen using factory | | +-- creates DashboardScreen using factory | +----------------------------+--------------------------+ | v +-------------------------------------------------------+ | ABSTRACT FACTORY LAYER | | | | UIFactory (interface) | | +-- createButton(text): Button | | +-- createTextField(placeholder): TextField | | +-- createCheckbox(label): Checkbox | | | | UIFactoryProvider (super factory) | | +-- getFactory(platform): UIFactory | +-------------------------------------------------------+ / \ +--------------------+ +---------------------+ | WindowsUIFactory | | MacOSUIFactory | | creates: | | creates: | | - WindowsButton | | - MacOSButton | | - WindowsTextField | | - MacOSTextField | | - WindowsCheckbox | | - MacOSCheckbox | +--------------------+ +---------------------+ | | v v +-------------------------------------------------------+ | ABSTRACT PRODUCT LAYER | | Button (interface) - render(), onClick() | | TextField (interface) - render(), getValue() | | Checkbox (interface) - render(), isChecked() | +-------------------------------------------------------+

Low-Level Design (LLD)

+---------------------------------------------+ | UIFactory (interface) | +---------------------------------------------+ | + createButton(text): Button | | + createTextField(placeholder): TextField | | + createCheckbox(label): Checkbox | +---------------------------------------------+ ^ ^ | | +-------------------+ +-------------------+ | WindowsUIFactory | | MacOSUIFactory | +-------------------+ +-------------------+ +---------------------------------------------+ | Button (interface) | +---------------------------------------------+ | + render(): void | | + onClick(action: Runnable): void | | + getText(): String | +---------------------------------------------+ ^ ^ | | +----------------+ +----------------+ | WindowsButton | | MacOSButton | | style: [====] | | style: (====) | +----------------+ +----------------+ +---------------------------------------------+ | TextField (interface) | +---------------------------------------------+ | + render(): void | | + setValue(text): void | | + getValue(): String | +---------------------------------------------+ ^ ^ | | +------------------+ +------------------+ | WindowsTextField | | MacOSTextField | | style: |_____| | | style: [_____| | +------------------+ +------------------+ +---------------------------------------------+ | Checkbox (interface) | +---------------------------------------------+ | + render(): void | | + toggle(): void | | + isChecked(): boolean | +---------------------------------------------+ ^ ^ | | +------------------+ +------------------+ | WindowsCheckbox | | MacOSCheckbox | | style: [x] | | style: (x) | +------------------+ +------------------+ +---------------------------------------------+ | UIFactoryProvider (super factory) | +---------------------------------------------+ | + getFactory(platform): UIFactory [static] | | Reads platform, returns correct factory. | | Adding Linux = add LinuxUIFactory + products| | No changes to existing code. | +---------------------------------------------+ +---------------------------------------------+ | Screen classes (use factory) | +---------------------------------------------+ | LoginScreen | | - factory: UIFactory | | - usernameField, passwordField, loginButton | | + render(): void | +---------------------------------------------+ | DashboardScreen | | - factory: UIFactory | | - welcomeButton, settingsCheckbox | | + render(): void | +---------------------------------------------+

Project Structure

cross-platform-ui-demo/ ├── src/ │ └── main/java/com/nbi/ui/ │ ├── product/ │ │ ├── Button.java (interface) │ │ ├── TextField.java (interface) │ │ ├── Checkbox.java (interface) │ │ ├── windows/ │ │ │ ├── WindowsButton.java │ │ │ ├── WindowsTextField.java │ │ │ └── WindowsCheckbox.java │ │ └── macos/ │ │ ├── MacOSButton.java │ │ ├── MacOSTextField.java │ │ └── MacOSCheckbox.java │ ├── factory/ │ │ ├── UIFactory.java (abstract factory interface) │ │ ├── WindowsUIFactory.java │ │ ├── MacOSUIFactory.java │ │ └── UIFactoryProvider.java (super factory) │ ├── screen/ │ │ ├── LoginScreen.java │ │ └── DashboardScreen.java │ └── Application.java ├── src/ │ └── test/java/com/nbi/ui/ │ ├── factory/ │ │ └── UIFactoryProviderTest.java │ └── screen/ │ └── LoginScreenTest.java ├── src/ │ └── main/resources/ │ └── ui.properties └── pom.xml

ui.properties

# Change this to switch the entire UI family
# Supported: windows, macos
ui.platform=windows

Product interfaces

// Button.java
package com.nbi.ui.product;

public interface Button {
    void render();
    void onClick(Runnable action);
    String getText();
}

// TextField.java
package com.nbi.ui.product;

public interface TextField {
    void render();
    void setValue(String text);
    String getValue();
}

// Checkbox.java
package com.nbi.ui.product;

public interface Checkbox {
    void render();
    void toggle();
    boolean isChecked();
}

Windows products

// WindowsButton.java
package com.nbi.ui.product.windows;

import com.nbi.ui.product.Button;

public class WindowsButton implements Button {
    private final String text;

    public WindowsButton(String text) {
        this.text = text;
    }

    @Override
    public void render() {
        System.out.println("  [Windows Button] [ " + text + " ]");
    }

    @Override
    public void onClick(Runnable action) {
        System.out.println("  [Windows Button] clicked: " + text);
        action.run();
    }

    @Override
    public String getText() { return text; }
}

// WindowsTextField.java
package com.nbi.ui.product.windows;

import com.nbi.ui.product.TextField;

public class WindowsTextField implements TextField {
    private final String placeholder;
    private String value = "";

    public WindowsTextField(String placeholder) {
        this.placeholder = placeholder;
    }

    @Override
    public void render() {
        String display = value.isEmpty() ? placeholder : value;
        System.out.println("  [Windows TextField] |" + display + "|");
    }

    @Override
    public void setValue(String text) { this.value = text; }

    @Override
    public String getValue() { return value; }
}

// WindowsCheckbox.java
package com.nbi.ui.product.windows;

import com.nbi.ui.product.Checkbox;

public class WindowsCheckbox implements Checkbox {
    private final String label;
    private boolean checked = false;

    public WindowsCheckbox(String label) {
        this.label = label;
    }

    @Override
    public void render() {
        String mark = checked ? "x" : " ";
        System.out.println("  [Windows Checkbox] [" + mark + "] " + label);
    }

    @Override
    public void toggle() { checked = !checked; }

    @Override
    public boolean isChecked() { return checked; }
}

macOS products

// MacOSButton.java
package com.nbi.ui.product.macos;

import com.nbi.ui.product.Button;

public class MacOSButton implements Button {
    private final String text;

    public MacOSButton(String text) {
        this.text = text;
    }

    @Override
    public void render() {
        System.out.println("  (macOS Button) ( " + text + " )");
    }

    @Override
    public void onClick(Runnable action) {
        System.out.println("  (macOS Button) tapped: " + text);
        action.run();
    }

    @Override
    public String getText() { return text; }
}

// MacOSTextField.java
package com.nbi.ui.product.macos;

import com.nbi.ui.product.TextField;

public class MacOSTextField implements TextField {
    private final String placeholder;
    private String value = "";

    public MacOSTextField(String placeholder) {
        this.placeholder = placeholder;
    }

    @Override
    public void render() {
        String display = value.isEmpty() ? placeholder : value;
        System.out.println("  (macOS TextField) [" + display + "]");
    }

    @Override
    public void setValue(String text) { this.value = text; }

    @Override
    public String getValue() { return value; }
}

// MacOSCheckbox.java
package com.nbi.ui.product.macos;

import com.nbi.ui.product.Checkbox;

public class MacOSCheckbox implements Checkbox {
    private final String label;
    private boolean checked = false;

    public MacOSCheckbox(String label) {
        this.label = label;
    }

    @Override
    public void render() {
        String mark = checked ? "x" : " ";
        System.out.println("  (macOS Checkbox) (" + mark + ") " + label);
    }

    @Override
    public void toggle() { checked = !checked; }

    @Override
    public boolean isChecked() { return checked; }
}

UIFactory.java (Abstract Factory interface)

package com.nbi.ui.factory;

import com.nbi.ui.product.Button;
import com.nbi.ui.product.TextField;
import com.nbi.ui.product.Checkbox;

public interface UIFactory {
    Button createButton(String text);
    TextField createTextField(String placeholder);
    Checkbox createCheckbox(String label);
    String getPlatformName();
}

WindowsUIFactory.java

package com.nbi.ui.factory;

import com.nbi.ui.product.Button;
import com.nbi.ui.product.TextField;
import com.nbi.ui.product.Checkbox;
import com.nbi.ui.product.windows.WindowsButton;
import com.nbi.ui.product.windows.WindowsTextField;
import com.nbi.ui.product.windows.WindowsCheckbox;

public class WindowsUIFactory implements UIFactory {
    @Override
    public Button createButton(String text) { return new WindowsButton(text); }

    @Override
    public TextField createTextField(String placeholder) { return new WindowsTextField(placeholder); }

    @Override
    public Checkbox createCheckbox(String label) { return new WindowsCheckbox(label); }

    @Override
    public String getPlatformName() { return "Windows 11"; }
}

MacOSUIFactory.java

package com.nbi.ui.factory;

import com.nbi.ui.product.Button;
import com.nbi.ui.product.TextField;
import com.nbi.ui.product.Checkbox;
import com.nbi.ui.product.macos.MacOSButton;
import com.nbi.ui.product.macos.MacOSTextField;
import com.nbi.ui.product.macos.MacOSCheckbox;

public class MacOSUIFactory implements UIFactory {
    @Override
    public Button createButton(String text) { return new MacOSButton(text); }

    @Override
    public TextField createTextField(String placeholder) { return new MacOSTextField(placeholder); }

    @Override
    public Checkbox createCheckbox(String label) { return new MacOSCheckbox(label); }

    @Override
    public String getPlatformName() { return "macOS Sonoma"; }
}

UIFactoryProvider.java (Super Factory)

package com.nbi.ui.factory;

public class UIFactoryProvider {

    public static UIFactory getFactory(String platform) {
        if (platform == null) {
            throw new IllegalArgumentException("Platform cannot be null");
        }

        switch (platform.toLowerCase()) {
            case "windows":
                return new WindowsUIFactory();
            case "macos":
            case "mac":
                return new MacOSUIFactory();
            default:
                throw new IllegalArgumentException(
                    "Unsupported platform: " + platform
                    + ". Supported: windows, macos");
        }
    }
}

LoginScreen.java

package com.nbi.ui.screen;

import com.nbi.ui.factory.UIFactory;
import com.nbi.ui.product.Button;
import com.nbi.ui.product.TextField;
import com.nbi.ui.product.Checkbox;

public class LoginScreen {
    private final UIFactory factory;
    private final TextField usernameField;
    private final TextField passwordField;
    private final Checkbox rememberMe;
    private final Button loginButton;

    public LoginScreen(UIFactory factory) {
        this.factory = factory;
        this.usernameField = factory.createTextField("Enter username");
        this.passwordField = factory.createTextField("Enter password");
        this.rememberMe = factory.createCheckbox("Remember me");
        this.loginButton = factory.createButton("Login");
    }

    public void render() {
        System.out.println("--- Login Screen (" + factory.getPlatformName() + ") ---");
        usernameField.render();
        passwordField.render();
        rememberMe.render();
        loginButton.render();
    }

    public void simulateLogin(String username, String password) {
        usernameField.setValue(username);
        passwordField.setValue(password);
        rememberMe.toggle();

        System.out.println("\nAfter user input:");
        usernameField.render();
        passwordField.render();
        rememberMe.render();

        loginButton.onClick(() ->
            System.out.println("  Logging in as: " + usernameField.getValue()));
    }
}

DashboardScreen.java

package com.nbi.ui.screen;

import com.nbi.ui.factory.UIFactory;
import com.nbi.ui.product.Button;
import com.nbi.ui.product.Checkbox;

public class DashboardScreen {
    private final UIFactory factory;
    private final Button transferButton;
    private final Button balanceButton;
    private final Checkbox notificationsCheckbox;

    public DashboardScreen(UIFactory factory) {
        this.factory = factory;
        this.transferButton = factory.createButton("Transfer Money");
        this.balanceButton = factory.createButton("Check Balance");
        this.notificationsCheckbox = factory.createCheckbox("Enable notifications");
    }

    public void render() {
        System.out.println("--- Dashboard (" + factory.getPlatformName() + ") ---");
        transferButton.render();
        balanceButton.render();
        notificationsCheckbox.render();
    }
}

Application.java

package com.nbi.ui;

import com.nbi.ui.factory.UIFactory;
import com.nbi.ui.factory.UIFactoryProvider;
import com.nbi.ui.screen.LoginScreen;
import com.nbi.ui.screen.DashboardScreen;
import java.io.InputStream;
import java.util.Properties;

public class Application {
    public static void main(String[] args) {
        // Read platform from config
        String platform = readPlatform();
        System.out.println("Platform: " + platform);
        System.out.println();

        // Get factory for the platform
        UIFactory factory = UIFactoryProvider.getFactory(platform);

        // Scenario 1: Render Login Screen
        LoginScreen loginScreen = new LoginScreen(factory);
        loginScreen.render();
        System.out.println();

        // Scenario 2: Simulate user login
        loginScreen.simulateLogin("rajesh.kumar", "nbi@2026");
        System.out.println();

        // Scenario 3: Render Dashboard
        DashboardScreen dashboard = new DashboardScreen(factory);
        dashboard.render();
        System.out.println();

        // Scenario 4: Switch platform at runtime
        System.out.println("=== Switching to macOS ===");
        UIFactory macFactory = UIFactoryProvider.getFactory("macos");
        LoginScreen macLogin = new LoginScreen(macFactory);
        macLogin.render();
        System.out.println();

        DashboardScreen macDash = new DashboardScreen(macFactory);
        macDash.render();
    }

    private static String readPlatform() {
        try (InputStream is = Application.class.getClassLoader()
                .getResourceAsStream("ui.properties")) {
            if (is != null) {
                Properties props = new Properties();
                props.load(is);
                return props.getProperty("ui.platform", "windows");
            }
        } catch (Exception e) {
            System.out.println("Could not read config, defaulting to windows");
        }
        return "windows";
    }
}

Expected Output

Platform: windows --- Login Screen (Windows 11) --- [Windows TextField] |Enter username| [Windows TextField] |Enter password| [Windows Checkbox] [ ] Remember me [Windows Button] [ Login ] After user input: [Windows TextField] |rajesh.kumar| [Windows TextField] |nbi@2026| [Windows Checkbox] [x] Remember me [Windows Button] clicked: Login Logging in as: rajesh.kumar --- Dashboard (Windows 11) --- [Windows Button] [ Transfer Money ] [Windows Button] [ Check Balance ] [Windows Checkbox] [ ] Enable notifications === Switching to macOS === --- Login Screen (macOS Sonoma) --- (macOS TextField) [Enter username] (macOS TextField) [Enter password] (macOS Checkbox) ( ) Remember me (macOS Button) ( Login ) --- Dashboard (macOS Sonoma) --- (macOS Button) ( Transfer Money ) (macOS Button) ( Check Balance ) (macOS Checkbox) ( ) Enable notifications

UIFactoryProviderTest.java

package com.nbi.ui.factory;

import com.nbi.ui.product.Button;
import com.nbi.ui.product.TextField;
import com.nbi.ui.product.Checkbox;
import com.nbi.ui.product.windows.WindowsButton;
import com.nbi.ui.product.macos.MacOSButton;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class UIFactoryProviderTest {

    @Test
    void getFactory_returnsWindowsFactory() {
        UIFactory factory = UIFactoryProvider.getFactory("windows");
        Button btn = factory.createButton("Test");
        assertTrue(btn instanceof WindowsButton);
    }

    @Test
    void getFactory_returnsMacOSFactory() {
        UIFactory factory = UIFactoryProvider.getFactory("macos");
        Button btn = factory.createButton("Test");
        assertTrue(btn instanceof MacOSButton);
    }

    @Test
    void getFactory_isCaseInsensitive() {
        UIFactory f1 = UIFactoryProvider.getFactory("WINDOWS");
        UIFactory f2 = UIFactoryProvider.getFactory("windows");
        assertEquals(f1.getPlatformName(), f2.getPlatformName());
    }

    @Test
    void getFactory_throwsForNull() {
        assertThrows(IllegalArgumentException.class,
            () -> UIFactoryProvider.getFactory(null));
    }

    @Test
    void getFactory_throwsForUnsupported() {
        assertThrows(IllegalArgumentException.class,
            () -> UIFactoryProvider.getFactory("linux"));
    }

    @Test
    void windowsFactory_createsConsistentFamily() {
        UIFactory factory = UIFactoryProvider.getFactory("windows");
        assertNotNull(factory.createButton("btn"));
        assertNotNull(factory.createTextField("field"));
        assertNotNull(factory.createCheckbox("check"));
    }

    @Test
    void macFactory_createsConsistentFamily() {
        UIFactory factory = UIFactoryProvider.getFactory("macos");
        assertNotNull(factory.createButton("btn"));
        assertNotNull(factory.createTextField("field"));
        assertNotNull(factory.createCheckbox("check"));
    }

    @Test
    void productsFromSameFactory_areConsistent() {
        UIFactory winFactory = UIFactoryProvider.getFactory("windows");
        UIFactory macFactory = UIFactoryProvider.getFactory("macos");

        // Windows products should be Windows type
        assertTrue(winFactory.createButton("x") instanceof WindowsButton);
        // macOS products should be macOS type
        assertTrue(macFactory.createButton("x") instanceof MacOSButton);
    }
}

Key observations from the project

  1. LoginScreen and DashboardScreen never import platform-specific classes. They work only with UIFactory, Button, TextField, Checkbox interfaces.
  2. Switching platforms requires changing one config value (ui.platform=macos). Zero code changes.
  3. Adding Linux support: Create LinuxButton, LinuxTextField, LinuxCheckbox, LinuxUIFactory, and add one case to UIFactoryProvider. No existing file is modified.
  4. Products within a family are consistent. WindowsUIFactory only creates Windows components. You cannot accidentally mix a WindowsButton with a MacOSTextField.
  5. The super factory hides all concrete factories. The client calls UIFactoryProvider.getFactory("windows") and gets back a UIFactory interface. It never knows about WindowsUIFactory.

7. When to Use and When NOT to Use

When to use Abstract Factory

ScenarioExampleWhy Abstract Factory
Multiple product familiesJDBC vs Hibernate DAO implementationsEach family has multiple related products that must work together
Cross-platform UIWindows vs macOS vs Linux componentsButtons, fields, checkboxes must all be from the same platform
Theme systemsLight theme vs Dark theme componentsAll themed components must match
Multi-vendor integrationAWS vs Azure cloud servicesStorage, compute, and messaging must all be from the same vendor
Test vs ProductionReal services vs Mock servicesAll dependencies must be consistently real or mock

When NOT to use Abstract Factory

ScenarioWhy NOTUse Instead
Single product typeOnly one product, not a familySimple Factory or Factory Method
Products are independentButton and database connection are unrelatedSeparate factories
Only one family existsNo need for abstraction over one implementationDirect instantiation
Simple applicationAbstract Factory adds many classes for simple casesSimple Factory

8. Quick Reference Card

AspectSimple FactoryFactory MethodAbstract Factory
GOF Pattern?No (informal)YesYes
CreatesOne product typeOne product typeFamily of products
Factories1 factory classN factory subclassesN family factories
ExtensionModify factory (violates OCP)Add factory subclassAdd entire factory family
ConsistencyNot guaranteedPer-factoryEntire family consistent
ComplexityLowMediumHigh
JDK exampleCalendar.getInstance()Collection.iterator()javax.xml.parsers.DocumentBuilderFactory