Abstract Factory Pattern
1. What is Abstract Factory Pattern?
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
| Pattern | Creates | Structure | Analogy |
|---|---|---|---|
| Simple Factory | One product type, multiple variants | 1 factory, N products | One bakery making different cakes |
| Factory Method | One product type, factory per variant | 1 abstract factory, N factory subclasses | Multiple bakeries (Chennai, Noida) each making cakes their way |
| Abstract Factory | Multiple product families | 1 super factory returning N factories, each producing a family | A food court that has a bakery, a juice bar, and a snack counter |
2. The 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
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.
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();
}
}
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.
// 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"));
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.
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
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)
Low-Level Design (LLD)
Project Structure
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
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
- LoginScreen and DashboardScreen never import platform-specific classes. They work only with
UIFactory,Button,TextField,Checkboxinterfaces. - Switching platforms requires changing one config value (
ui.platform=macos). Zero code changes. - Adding Linux support: Create
LinuxButton,LinuxTextField,LinuxCheckbox,LinuxUIFactory, and add one case toUIFactoryProvider. No existing file is modified. - Products within a family are consistent. WindowsUIFactory only creates Windows components. You cannot accidentally mix a WindowsButton with a MacOSTextField.
- The super factory hides all concrete factories. The client calls
UIFactoryProvider.getFactory("windows")and gets back aUIFactoryinterface. It never knows aboutWindowsUIFactory.
7. When to Use and When NOT to Use
When to use Abstract Factory
| Scenario | Example | Why Abstract Factory |
|---|---|---|
| Multiple product families | JDBC vs Hibernate DAO implementations | Each family has multiple related products that must work together |
| Cross-platform UI | Windows vs macOS vs Linux components | Buttons, fields, checkboxes must all be from the same platform |
| Theme systems | Light theme vs Dark theme components | All themed components must match |
| Multi-vendor integration | AWS vs Azure cloud services | Storage, compute, and messaging must all be from the same vendor |
| Test vs Production | Real services vs Mock services | All dependencies must be consistently real or mock |
When NOT to use Abstract Factory
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Single product type | Only one product, not a family | Simple Factory or Factory Method |
| Products are independent | Button and database connection are unrelated | Separate factories |
| Only one family exists | No need for abstraction over one implementation | Direct instantiation |
| Simple application | Abstract Factory adds many classes for simple cases | Simple Factory |
8. Quick Reference Card
| Aspect | Simple Factory | Factory Method | Abstract Factory |
|---|---|---|---|
| GOF Pattern? | No (informal) | Yes | Yes |
| Creates | One product type | One product type | Family of products |
| Factories | 1 factory class | N factory subclasses | N family factories |
| Extension | Modify factory (violates OCP) | Add factory subclass | Add entire factory family |
| Consistency | Not guaranteed | Per-factory | Entire family consistent |
| Complexity | Low | Medium | High |
| JDK example | Calendar.getInstance() | Collection.iterator() | javax.xml.parsers.DocumentBuilderFactory |
Abstract Factory Design Pattern in Java
1. What is Abstract Factory Pattern?
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
| Pattern | Creates | Structure | Analogy |
|---|---|---|---|
| Simple Factory | One product type, multiple variants | 1 factory, N products | One bakery making different cakes |
| Factory Method | One product type, factory per variant | 1 abstract factory, N factory subclasses | Multiple bakeries (Chennai, Noida) each making cakes their way |
| Abstract Factory | Multiple product families | 1 super factory returning N factories, each producing a family | A food court that has a bakery, a juice bar, and a snack counter |
2. The 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
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.
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();
}
}
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.
// 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"));
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.
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
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)
Low-Level Design (LLD)
Project Structure
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
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
- LoginScreen and DashboardScreen never import platform-specific classes. They work only with
UIFactory,Button,TextField,Checkboxinterfaces. - Switching platforms requires changing one config value (
ui.platform=macos). Zero code changes. - Adding Linux support: Create
LinuxButton,LinuxTextField,LinuxCheckbox,LinuxUIFactory, and add one case toUIFactoryProvider. No existing file is modified. - Products within a family are consistent. WindowsUIFactory only creates Windows components. You cannot accidentally mix a WindowsButton with a MacOSTextField.
- The super factory hides all concrete factories. The client calls
UIFactoryProvider.getFactory("windows")and gets back aUIFactoryinterface. It never knows aboutWindowsUIFactory.
7. When to Use and When NOT to Use
When to use Abstract Factory
| Scenario | Example | Why Abstract Factory |
|---|---|---|
| Multiple product families | JDBC vs Hibernate DAO implementations | Each family has multiple related products that must work together |
| Cross-platform UI | Windows vs macOS vs Linux components | Buttons, fields, checkboxes must all be from the same platform |
| Theme systems | Light theme vs Dark theme components | All themed components must match |
| Multi-vendor integration | AWS vs Azure cloud services | Storage, compute, and messaging must all be from the same vendor |
| Test vs Production | Real services vs Mock services | All dependencies must be consistently real or mock |
When NOT to use Abstract Factory
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Single product type | Only one product, not a family | Simple Factory or Factory Method |
| Products are independent | Button and database connection are unrelated | Separate factories |
| Only one family exists | No need for abstraction over one implementation | Direct instantiation |
| Simple application | Abstract Factory adds many classes for simple cases | Simple Factory |
8. Quick Reference Card
| Aspect | Simple Factory | Factory Method | Abstract Factory |
|---|---|---|---|
| GOF Pattern? | No (informal) | Yes | Yes |
| Creates | One product type | One product type | Family of products |
| Factories | 1 factory class | N factory subclasses | N family factories |
| Extension | Modify factory (violates OCP) | Add factory subclass | Add entire factory family |
| Consistency | Not guaranteed | Per-factory | Entire family consistent |
| Complexity | Low | Medium | High |
| JDK example | Calendar.getInstance() | Collection.iterator() | javax.xml.parsers.DocumentBuilderFactory |