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
- If there's only one product, or the family never grows past one member, a plain Factory Method says the same thing with less machinery.
- If new product kinds get added often (not new families, but new members within a family —
e.g. adding a
SlideralongsideButton/Checkbox), every concrete factory needs a new method, which means editing every existing implementation — the classic Abstract Factory trade-off of "easy to add a family, hard to add a product kind." - Don't reach for it just because two classes happen to be constructed near each other. The point is enforcing that they can only be constructed together as a matching set — if mixing them would still be valid, this pattern is solving a problem that doesn't exist here.
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
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. — Chapter 3 formalizes Abstract Factory.
- Johnson, R., & Foote, B. (1988). "Designing Reusable Classes." Journal of Object-Oriented Programming, 1(2), 22-35. — early formalization of the "protocol" a family of related classes must share, the same family-consistency idea this pattern encodes structurally.
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");
}
}