You’re building a cross-platform UI library. On Windows, buttons and text boxes look one way. On macOS, they look different. On Linux, different again. Every component in a platform must match the others.

You can’t mix a Windows button with a macOS text box. They need to come from the same family.

What is the Abstract Factory Pattern?

Abstract Factory provides an interface for creating families of related objects. Each concrete factory produces objects that work together. The client uses the factory interface without knowing which concrete factory it’s using.

Abstract Factory Design Pattern class diagram showing UIFactory interface with WinFactory and MacFactory implementations creating platform-specific Button and TextBox products

The key: WinFactory creates WinButton and WinTextBox (they go together). MacFactory creates MacButton and MacTextBox (they go together). You never mix products from different factories.

When to Use Abstract Factory

Use Abstract Factory When Skip Abstract Factory When
You need families of related objects Objects don’t need to be consistent
Products in a family must work together Products are independent
You want to enforce consistency Only one product type exists
Switching families should be easy Factory Method is sufficient

The pattern shines when product compatibility matters. Wrong combinations should be impossible.

Implementation

Cross-Platform UI Toolkit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Product interfaces
public interface Button {
    void render();
    void onClick(Runnable handler);
}

public interface TextBox {
    void render();
    String getText();
    void setText(String text);
}

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

// Abstract Factory
public interface UIFactory {
    Button createButton(String label);
    TextBox createTextBox();
    Checkbox createCheckbox(String label);
}

Windows Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class WindowsButton implements Button {
    private final String label;
    private Runnable clickHandler;
    
    public WindowsButton(String label) {
        this.label = label;
    }
    
    @Override
    public void render() {
        System.out.println("[====== " + label + " ======]");  // Windows style
    }
    
    @Override
    public void onClick(Runnable handler) {
        this.clickHandler = handler;
    }
}

public class WindowsTextBox implements TextBox {
    private String text = "";
    
    @Override
    public void render() {
        System.out.println("|__" + text + "__|");  // Windows style border
    }
    
    @Override
    public String getText() { return text; }
    
    @Override
    public void setText(String text) { this.text = text; }
}

public class WindowsCheckbox implements Checkbox {
    private final String label;
    private boolean checked;
    
    public WindowsCheckbox(String label) {
        this.label = label;
    }
    
    @Override
    public void render() {
        String box = checked ? "[X]" : "[ ]";
        System.out.println(box + " " + label);
    }
    
    @Override
    public boolean isChecked() { return checked; }
    
    @Override
    public void setChecked(boolean checked) { this.checked = checked; }
}

public class WindowsUIFactory implements UIFactory {
    @Override
    public Button createButton(String label) {
        return new WindowsButton(label);
    }
    
    @Override
    public TextBox createTextBox() {
        return new WindowsTextBox();
    }
    
    @Override
    public Checkbox createCheckbox(String label) {
        return new WindowsCheckbox(label);
    }
}

macOS Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class MacButton implements Button {
    private final String label;
    private Runnable clickHandler;
    
    public MacButton(String label) {
        this.label = label;
    }
    
    @Override
    public void render() {
        System.out.println("( " + label + " )");  // Rounded Mac style
    }
    
    @Override
    public void onClick(Runnable handler) {
        this.clickHandler = handler;
    }
}

public class MacTextBox implements TextBox {
    private String text = "";
    
    @Override
    public void render() {
        System.out.println("⌈ " + text + " ⌉");  // Mac style
    }
    
    @Override
    public String getText() { return text; }
    
    @Override
    public void setText(String text) { this.text = text; }
}

public class MacCheckbox implements Checkbox {
    private final String label;
    private boolean checked;
    
    public MacCheckbox(String label) {
        this.label = label;
    }
    
    @Override
    public void render() {
        String box = checked ? "●" : "○";
        System.out.println(box + " " + label);  // Mac-style circles
    }
    
    @Override
    public boolean isChecked() { return checked; }
    
    @Override
    public void setChecked(boolean checked) { this.checked = checked; }
}

public class MacUIFactory implements UIFactory {
    @Override
    public Button createButton(String label) {
        return new MacButton(label);
    }
    
    @Override
    public TextBox createTextBox() {
        return new MacTextBox();
    }
    
    @Override
    public Checkbox createCheckbox(String label) {
        return new MacCheckbox(label);
    }
}

Factory Provider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class UIFactoryProvider {
    
    public static UIFactory getFactory() {
        String os = System.getProperty("os.name").toLowerCase();
        
        if (os.contains("win")) {
            return new WindowsUIFactory();
        } else if (os.contains("mac")) {
            return new MacUIFactory();
        } else {
            return new LinuxUIFactory();  // Default
        }
    }
    
    public static UIFactory getFactory(String platform) {
        switch (platform.toLowerCase()) {
            case "windows": return new WindowsUIFactory();
            case "mac": return new MacUIFactory();
            case "linux": return new LinuxUIFactory();
            default: throw new IllegalArgumentException("Unknown platform: " + platform);
        }
    }
}

Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class LoginForm {
    private final Button loginButton;
    private final Button cancelButton;
    private final TextBox usernameField;
    private final TextBox passwordField;
    private final Checkbox rememberMe;
    
    public LoginForm(UIFactory factory) {
        // All components from the same factory = consistent look
        this.usernameField = factory.createTextBox();
        this.passwordField = factory.createTextBox();
        this.loginButton = factory.createButton("Login");
        this.cancelButton = factory.createButton("Cancel");
        this.rememberMe = factory.createCheckbox("Remember me");
        
        loginButton.onClick(this::handleLogin);
        cancelButton.onClick(this::handleCancel);
    }
    
    public void render() {
        System.out.println("Username:");
        usernameField.render();
        System.out.println("Password:");
        passwordField.render();
        rememberMe.render();
        loginButton.render();
        cancelButton.render();
    }
    
    private void handleLogin() { /* ... */ }
    private void handleCancel() { /* ... */ }
}

// Usage
UIFactory factory = UIFactoryProvider.getFactory();
LoginForm loginForm = new LoginForm(factory);
loginForm.render();

// On Windows:
// Username:
// |__|
// Password:
// |__|
// [ ] Remember me
// [====== Login ======]
// [====== Cancel ======]

// On Mac:
// Username:
// ⌈  ⌉
// Password:
// ⌈  ⌉
// ○ Remember me
// ( Login )
// ( Cancel )

How It Works

sequenceDiagram
    participant Client as LoginForm
    participant Provider as UIFactoryProvider
    participant Factory as MacUIFactory
    participant Button as MacButton
    participant TextBox as MacTextBox
    
    Client->>Provider: getFactory()
    Provider->>Provider: Check OS
    Provider->>Factory: Create factory
    Factory-->>Client: MacUIFactory
    
    Client->>Factory: createTextBox()
    Factory->>TextBox: new MacTextBox()
    TextBox-->>Client: MacTextBox instance
    
    Client->>Factory: createButton("Login")
    Factory->>Button: new MacButton("Login")
    Button-->>Client: MacButton instance
    
    Note over Client: All products are Mac-style

Database Provider Example

Another common use case: database-agnostic code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public interface DatabaseFactory {
    Connection createConnection(String connectionString);
    QueryBuilder createQueryBuilder();
    MigrationRunner createMigrationRunner();
}

public class PostgresFactory implements DatabaseFactory {
    @Override
    public Connection createConnection(String connectionString) {
        return new PostgresConnection(connectionString);
    }
    
    @Override
    public QueryBuilder createQueryBuilder() {
        return new PostgresQueryBuilder();  // Handles Postgres-specific SQL
    }
    
    @Override
    public MigrationRunner createMigrationRunner() {
        return new PostgresMigrationRunner();
    }
}

public class MySqlFactory implements DatabaseFactory {
    @Override
    public Connection createConnection(String connectionString) {
        return new MySqlConnection(connectionString);
    }
    
    @Override
    public QueryBuilder createQueryBuilder() {
        return new MySqlQueryBuilder();  // Handles MySQL-specific SQL
    }
    
    @Override
    public MigrationRunner createMigrationRunner() {
        return new MySqlMigrationRunner();
    }
}

// Application code works with any database
public class UserRepository {
    private final Connection connection;
    private final QueryBuilder queryBuilder;
    
    public UserRepository(DatabaseFactory factory, String connString) {
        this.connection = factory.createConnection(connString);
        this.queryBuilder = factory.createQueryBuilder();
    }
    
    public User findById(String id) {
        String query = queryBuilder
            .select("*")
            .from("users")
            .where("id", id)
            .build();
        
        return connection.querySingle(query, User.class);
    }
}

Abstract Factory vs Factory Method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// FACTORY METHOD - one product, inheritance
public abstract class Dialog {
    public abstract Button createButton();  // Factory method
    
    public void render() {
        Button button = createButton();
        button.render();
    }
}

public class WindowsDialog extends Dialog {
    @Override
    public Button createButton() {
        return new WindowsButton();
    }
}

// ABSTRACT FACTORY - family of products, composition
public interface UIFactory {
    Button createButton();
    TextBox createTextBox();
    Checkbox createCheckbox();
}

public class Application {
    private final UIFactory factory;  // Injected
    
    public Application(UIFactory factory) {
        this.factory = factory;
    }
}

Factory Method: one product, subclass decides. Abstract Factory: multiple products, entire family together.

Common Mistakes

1. Adding Products to All Factories

Adding a new product type requires changing all factory implementations:

1
2
3
4
5
6
// Adding Slider to UI means updating WindowsFactory, MacFactory, LinuxFactory...
public interface UIFactory {
    Button createButton();
    TextBox createTextBox();
    Slider createSlider();  // New - must implement everywhere
}

This is a known trade-off. Abstract Factory makes adding new families easy but adding new products hard.

2. Factory Creating Unrelated Products

Products should form a cohesive family:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Bad - unrelated products
public interface StrangeFactory {
    Button createButton();
    HttpClient createHttpClient();  // What does this have to do with buttons?
    EmailService createEmailService();  // Not a family
}

// Good - cohesive family
public interface UIFactory {
    Button createButton();
    TextBox createTextBox();
    Checkbox createCheckbox();
    // All UI components
}

3. Exposing Concrete Types

Clients should work with interfaces only:

1
2
3
4
5
6
7
// Wrong - exposes concrete type
WindowsButton button = (WindowsButton) factory.createButton();
button.setWindowsSpecificProperty(value);

// Right - works with interface
Button button = factory.createButton();
button.render();

Real-World Examples

Java AWT/Swing: Toolkit is an abstract factory for GUI components.

JDBC: DriverManager returns database-specific connections.

Document Builders: XML parsers use DocumentBuilderFactory.

Spring: BeanFactory is an abstract factory for beans.

Factory Method is a simpler version for single products. Abstract Factory often contains multiple factory methods.

Singleton is often used for factories. One factory instance per family.

Builder can work with Abstract Factory to construct complex products step by step.

Prototype is an alternative. Instead of factories, you clone configured prototypes.

Wrapping Up

Abstract Factory creates families of related objects. All products from one factory work together consistently. Switching families means switching factories.

Use it when products must be compatible, when consistency matters, and when you want to isolate product creation from usage.

The trade-off: adding new product types is hard because all factories must implement them. But adding new families is easy; just create a new factory.


Further Reading: