← All patterns

Abstract Factory

Creational · view source on GitHub

Read this in: English · Português · Español

Category: Creational

The problem

Some products only make sense in families: a Windows button next to a Mac checkbox looks and behaves wrong, a domestic policy document paired with an international premium rate is simply incorrect. If callers construct each product with its own new, nothing stops a family mismatch — the compiler can't see that WinButton and MacCheckbox were meant to travel together, and a typo or a copy-pasted line silently produces an inconsistent object graph.

The solution

Group the related creation methods behind one factory interface, one method per product in the family. A concrete factory implementation always returns products from the same family, so a caller that only depends on the factory interface (never on the concrete product classes) physically cannot mix families — there's no constructor call left for it to get wrong.

classDiagram
    class AbstractFactory {
        <<interface>>
        +createProductA() ProductA
        +createProductB() ProductB
    }
    class ConcreteFactory1
    class ConcreteFactory2
    class ProductA1
    class ProductA2
    class ProductB1
    class ProductB2
    AbstractFactory <|.. ConcreteFactory1
    AbstractFactory <|.. ConcreteFactory2
    ConcreteFactory1 ..> ProductA1 : creates
    ConcreteFactory1 ..> ProductB1 : creates
    ConcreteFactory2 ..> ProductA2 : creates
    ConcreteFactory2 ..> ProductB2 : creates

Classic example

classic/UiFactory is the textbook cross-platform UI toolkit: a Button and a Checkbox per family, WinUiFactory producing WinButton/WinCheckbox and MacUiFactory producing MacButton/MacCheckbox. UiFactoryTest asserts that each factory renders both of its components in that platform's own style, never the other one's.

Applied example: domestic vs. international insurance policy issuance

applied/InsuranceProductFactory produces a PolicyDocument and a PremiumCalculator as one family: DomesticInsuranceProductFactory always pairs a domestic-format document with the domestic 2% rate, InternationalInsuranceProductFactory always pairs the international-format document with the international 3.5% rate. InsuranceProductIssuer depends only on the InsuranceProductFactory interface — swapping the entire product family for a policy is one constructor argument, never a branch inside the issuance logic itself.

This module is also one of two in the catalog (alongside Singleton) that brings in Spring Context on purpose: DomesticInsuranceConfig is a @Configuration class whose @Bean methods are, in effect, the same creation methods as DomesticInsuranceProductFactory — just resolved by the container instead of called by hand. The pattern is the same either way; only who invokes the creation methods changes. InsuranceProductIssuerTest covers both hand-rolled factories end to end, and DomesticInsuranceConfigTest verifies the Spring-managed family is just as coherent.

When not to use it

Test coverage

100% instruction coverage (branch coverage reports "n/a" — nothing in this module branches; it's all straight-line delegation to the right family). Reproduce it yourself:

./gradlew :creational:abstractfactory:jacocoTestReport

Report at creational/abstractfactory/build/reports/jacoco/test/html/index.html.

Further reading

Unit tests

src/test/java/com/designpatterns/creational/abstractfactory/classic/UiFactoryTest.java
package com.designpatterns.creational.abstractfactory.classic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class UiFactoryTest {

    @Test
    void theWindowsFactoryProducesOnlyWindowsComponents() {
        UiFactory factory = new WinUiFactory();

        assertThat(factory.createButton().render()).isEqualTo("[Windows Button]");
        assertThat(factory.createCheckbox().render()).isEqualTo("[Windows Checkbox]");
    }

    @Test
    void theMacFactoryProducesOnlyMacComponents() {
        UiFactory factory = new MacUiFactory();

        assertThat(factory.createButton().render()).isEqualTo("(Mac Button)");
        assertThat(factory.createCheckbox().render()).isEqualTo("(Mac Checkbox)");
    }
}
src/test/java/com/designpatterns/creational/abstractfactory/applied/DomesticInsuranceConfigTest.java
package com.designpatterns.creational.abstractfactory.applied;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;

class DomesticInsuranceConfigTest {

    private AnnotationConfigApplicationContext context;

    @AfterEach
    void closeContext() {
        if (context != null) {
            context.close();
        }
    }

    @Test
    void theConfigurationClassProducesACoherentDomesticBeanFamily() {
        context = new AnnotationConfigApplicationContext(DomesticInsuranceConfig.class);

        PolicyDocument document = context.getBean(PolicyDocument.class);
        PremiumCalculator calculator = context.getBean(PremiumCalculator.class);

        assertThat(document).isInstanceOf(DomesticPolicyDocument.class);
        assertThat(calculator).isInstanceOf(DomesticPremiumCalculator.class);
        assertThat(calculator.calculatePremiumCents(100_000_00L)).isEqualTo(200000L);
    }
}
src/test/java/com/designpatterns/creational/abstractfactory/applied/InsuranceProductIssuerTest.java
package com.designpatterns.creational.abstractfactory.applied;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class InsuranceProductIssuerTest {

    @Test
    void theDomesticFactoryIssuesADomesticPolicyAtTheDomesticRate() {
        InsuranceProductIssuer issuer = new InsuranceProductIssuer(new DomesticInsuranceProductFactory());

        String result = issuer.issuePolicy("Maria Silva", 100_000_00L);

        assertThat(result).isEqualTo("APOLICE NACIONAL - Segurado: Maria Silva | Premium: 200000 cents");
    }

    @Test
    void theInternationalFactoryIssuesAnInternationalPolicyAtTheInternationalRate() {
        InsuranceProductIssuer issuer = new InsuranceProductIssuer(new InternationalInsuranceProductFactory());

        String result = issuer.issuePolicy("John Smith", 100_000_00L);

        assertThat(result).isEqualTo("INTERNATIONAL POLICY - Insured: John Smith | Premium: 350000 cents");
    }
}

View full JaCoCo coverage report →