Abstract Factory
Purpose
Select related products together and preserve their compatibility. Abstract Factory alone does
not make mixing impossible: callers can combine products from different factories, and a public
aggregate constructor can accept mismatched products. State the enforcement boundary: trusted
assembly with contract tests, validated family identities, family-typed APIs, or encapsulated
operations that never expose mixable products. Shared family identity may also require the same
transaction/session instance, not merely the same vendor or format.
If there is no invariant binding the products to each other, this is not Abstract Factory. It
is a bag of factory methods, and it should be several separate providers or none at all.
When it is the answer
There are 2+ product types that must agree with each other
AND creation or selection needs a coherent family boundary
→ Abstract Factory
The family is selected once per deployment (profile, environment)
→ dependency injection: one @Configuration per family.
Verify coherent wiring; profiles and qualifiers do not prove compatibility.
The family is selected per request / tenant / document / region
→ select a coherent provider or prebuilt family by key.
DI may supply that registry; use factory methods when creation varies.
Third-party code must contribute a whole family
→ Abstract Factory as the SPI shape (ServiceLoader
provider returning the family, not N providers).
When it is not
- One product type. That is Factory Method or a
Supplier; the "abstract" in the name is
precisely the multi-product part.
- The products are unrelated —
createRepository, createHttpClient, createClock. This
is a service locator with a factory's name, and it re-couples every caller to one type that
knows everything (gof-pattern-antipatterns).
- The family differs only in constants. Rates, endpoints, limits and timeouts are data. A
class per value is the commonest false Abstract Factory; use configuration instead.
- Only one family exists, and the second is speculative. This weakens the case, but does not
decide it: an interface can still be justified as a module or plugin boundary, an ownership
seam, or a stable port. Record that reason; otherwise defer the abstraction until a second
family reveals the real common contract.
- Testing was the only motivation. First prefer substituting collaborators at an existing
boundary (
@MockitoBean in Spring Framework 6.2+, or a test @Configuration). A production
family abstraction can still be warranted when the coherent in-memory family is itself a
useful contract, not merely a test hook.
Modern Java expression
A record of factory functions can package a small family without additional implementation
classes. Its constructor and suppliers still need compatibility, null, freshness and ownership
contracts; final references do not make captured state or products thread-safe. A record of
already-created products is a family bundle, not a factory of fresh products.
The examples target Java 17 without preview features (records and sealed types); pattern switches
over sealed hierarchies require Java 21 to avoid preview. Inspect the project's actual release and
dependencies; the pattern also works with ordinary classes on older Java without upgrades.
Classical Modern
───────────────────────────────── ────────────────────────────────────
interface ReportFactory record ReportFamily(
Renderer newRenderer() Supplier<Renderer> renderer,
Paginator newPaginator() Supplier<Paginator> paginator,
StyleSheet newStyleSheet() Supplier<StyleSheet> styles)
class PdfReportFactory implements static ReportFamily pdf()
class HtmlReportFactory implements static ReportFamily html()
selection: if/else or a Map Map<Format, ReportFamily>, or a
sealed Format with exhaustive switch
Keep the interface when a product needs more than construction from the family — shared
configuration, a supports() predicate, a lifecycle to close — or when third parties implement
it, since an interface is a stabler SPI contract than a record's component list.
Decision rules
IF the products can be used in any combination without breaking
THEN there is no family. Inject each product independently.
IF the family is fixed at startup by profile or property
THEN dependency injection. Do not add a factory the container calls once.
IF the family key arrives from a request, a tenant or a document
THEN Abstract Factory keyed by that value, with an explicit failure for
an unknown key — never a silent default family.
IF the key comes from outside the process
THEN validate it against the supported registry before selection. Never turn an
untrusted class name into reflective loading; an extensible plugin key need not
be a compile-time closed enum, but it still needs authorization and failure policy.
IF a new product is added to the family
THEN every implementation must change. If that is unacceptable, the
family is not stable enough for this pattern — reconsider.
IF the factory starts caching what it creates
THEN define sharing, eviction, closure and thread safety. A cache alone is neither Flyweight
nor Singleton; select those patterns only if their separate intent fits.
Cross-cutting checks
- Concurrency. A factory is normally a stateless immutable value shared by all threads —
keep it that way. The moment it holds mutable state (a cache, a counter, a "current family"
field) it needs a memory model argument, and a mutable
currentFamily field is a race that
hands out mixed families under load.
- Distribution. The pattern is process-local. A "remote factory" that returns handles to
objects living elsewhere is a Proxy problem with the failure semantics that implies
(
gof-proxy). Where families correspond to protocol or schema versions, the selection is
capability negotiation and needs an explicit unsupported-version path.
- Performance. The pattern does not imply allocation: a family may return cached, pooled or
newly constructed products. Dispatch may inline at stable call sites and may become
megamorphic with many implementations. Neither effect is a design-level reason to adopt or
reject the pattern; profile the actual construction and call sites
(
jit-inlining-and-escape-analysis).
- Testing. The legitimate testing benefit is a whole coherent in-memory family, which makes
integration-style tests fast without mocks. The illegitimate one is a factory added so that a
single collaborator can be stubbed — inject that collaborator instead.
Review checklist
References
- Decision and alternatives — the family-invariant
test, Abstract Factory against dependency injection,
Map<Key, Supplier>, ServiceLoader and
configuration, and how it differs from Factory Method and Builder. Read before introducing or
removing a factory interface.
- Worked example — a report-export family selected per request,
built first as a classical hierarchy and then as a record of suppliers, with the tenant-scoped
variant, the failure path for an unknown format, and what each version costs. Read when
implementing.
1---2name: gof-abstract-factory3description: Abstract Factory in modern Java: the pattern exists to keep a _family_ of related objects mutually consistent when the family varies, not to centralise construction. Covers the family invariant that justifies it, why dependency injection already resolves the deployment-time case, when per-request or per-tenant selection benefits from a family provider, and how to express it as a record of suppliers or a sealed provider rather than a four-level interface hierarchy. Use when a factory interface is proposed, when profile-specific object graphs are being built by hand, when a family of parser/renderer/validator types must never be mixed across formats, when a plugin SPI must supply several related types at once, or when reviewing a factory whose products have nothing to do with each other. Does not cover single-product creation (gof-factory-method), assembling one complex object (gof-builder), copying an existing instance (gof-prototype), or wiring policy in general (java-dependency-inversion).4---56# Abstract Factory78## Purpose910Select related products together and preserve their compatibility. Abstract Factory alone does11not make mixing impossible: callers can combine products from different factories, and a public12aggregate constructor can accept mismatched products. State the enforcement boundary: trusted13assembly with contract tests, validated family identities, family-typed APIs, or encapsulated14operations that never expose mixable products. Shared family identity may also require the same15transaction/session instance, not merely the same vendor or format.1617If there is no invariant binding the products to each other, this is not Abstract Factory. It18is a bag of factory methods, and it should be several separate providers or none at all.1920## When it is the answer2122```text23There are 2+ product types that must agree with each other24 AND creation or selection needs a coherent family boundary25 → Abstract Factory2627The family is selected once per deployment (profile, environment)28 → dependency injection: one @Configuration per family.29 Verify coherent wiring; profiles and qualifiers do not prove compatibility.3031The family is selected per request / tenant / document / region32 → select a coherent provider or prebuilt family by key.33 DI may supply that registry; use factory methods when creation varies.3435Third-party code must contribute a whole family36 → Abstract Factory as the SPI shape (ServiceLoader37 provider returning the family, not N providers).38```3940## When it is not4142- **One product type.** That is Factory Method or a `Supplier`; the "abstract" in the name is43 precisely the multi-product part.44- **The products are unrelated** — `createRepository`, `createHttpClient`, `createClock`. This45 is a service locator with a factory's name, and it re-couples every caller to one type that46 knows everything (`gof-pattern-antipatterns`).47- **The family differs only in constants.** Rates, endpoints, limits and timeouts are data. A48 class per value is the commonest false Abstract Factory; use configuration instead.49- **Only one family exists, and the second is speculative.** This weakens the case, but does not50 decide it: an interface can still be justified as a module or plugin boundary, an ownership51 seam, or a stable port. Record that reason; otherwise defer the abstraction until a second52 family reveals the real common contract.53- **Testing was the only motivation.** First prefer substituting collaborators at an existing54 boundary (`@MockitoBean` in Spring Framework 6.2+, or a test `@Configuration`). A production55 family abstraction can still be warranted when the coherent in-memory family is itself a56 useful contract, not merely a test hook.5758## Modern Java expression5960A record of factory functions can package a small family without additional implementation61classes. Its constructor and suppliers still need compatibility, null, freshness and ownership62contracts; final references do not make captured state or products thread-safe. A record of63already-created products is a family bundle, not a factory of fresh products.6465The examples target Java 17 without preview features (records and sealed types); pattern switches66over sealed hierarchies require Java 21 to avoid preview. Inspect the project's actual release and67dependencies; the pattern also works with ordinary classes on older Java without upgrades.6869```text70Classical Modern71───────────────────────────────── ────────────────────────────────────72interface ReportFactory record ReportFamily(73 Renderer newRenderer() Supplier<Renderer> renderer,74 Paginator newPaginator() Supplier<Paginator> paginator,75 StyleSheet newStyleSheet() Supplier<StyleSheet> styles)7677class PdfReportFactory implements static ReportFamily pdf()78class HtmlReportFactory implements static ReportFamily html()7980selection: if/else or a Map Map<Format, ReportFamily>, or a81 sealed Format with exhaustive switch82```8384Keep the interface when a product needs more than construction from the family — shared85configuration, a `supports()` predicate, a lifecycle to close — or when third parties implement86it, since an interface is a stabler SPI contract than a record's component list.8788## Decision rules8990```text91IF the products can be used in any combination without breaking92THEN there is no family. Inject each product independently.9394IF the family is fixed at startup by profile or property95THEN dependency injection. Do not add a factory the container calls once.9697IF the family key arrives from a request, a tenant or a document98THEN Abstract Factory keyed by that value, with an explicit failure for99 an unknown key — never a silent default family.100101IF the key comes from outside the process102THEN validate it against the supported registry before selection. Never turn an103 untrusted class name into reflective loading; an extensible plugin key need not104 be a compile-time closed enum, but it still needs authorization and failure policy.105106IF a new product is added to the family107THEN every implementation must change. If that is unacceptable, the108 family is not stable enough for this pattern — reconsider.109110IF the factory starts caching what it creates111THEN define sharing, eviction, closure and thread safety. A cache alone is neither Flyweight112 nor Singleton; select those patterns only if their separate intent fits.113```114115## Cross-cutting checks116117- **Concurrency.** A factory is normally a stateless immutable value shared by all threads —118 keep it that way. The moment it holds mutable state (a cache, a counter, a "current family"119 field) it needs a memory model argument, and a mutable `currentFamily` field is a race that120 hands out mixed families under load.121- **Distribution.** The pattern is process-local. A "remote factory" that returns handles to122 objects living elsewhere is a Proxy problem with the failure semantics that implies123 (`gof-proxy`). Where families correspond to protocol or schema versions, the selection is124 capability negotiation and needs an explicit unsupported-version path.125- **Performance.** The pattern does not imply allocation: a family may return cached, pooled or126 newly constructed products. Dispatch may inline at stable call sites and may become127 megamorphic with many implementations. Neither effect is a design-level reason to adopt or128 reject the pattern; profile the actual construction and call sites129 (`jit-inlining-and-escape-analysis`).130- **Testing.** The legitimate testing benefit is a whole coherent in-memory family, which makes131 integration-style tests fast without mocks. The illegitimate one is a factory added so that a132 single collaborator can be stubbed — inject that collaborator instead.133134## Review checklist135136- [ ] There are two or more products, and a stated invariant binds them137- [ ] The actual compatibility enforcement boundary is explicit and tested138- [ ] The selection key is named and validated against the authorized supported registry139- [ ] An unknown key fails loudly rather than falling back to a default family140- [ ] Multiple families exist, or a concrete SPI/module boundary justifies the abstraction141- [ ] Mutable factory, supplier and product state has an explicit concurrency/lifetime contract142- [ ] The products differ in behaviour, not only in configuration values143- [ ] Adding a product to the family is an acceptable change to every implementation144145## References146147- [Decision and alternatives](references/decision-and-alternatives.md) — the family-invariant148 test, Abstract Factory against dependency injection, `Map<Key, Supplier>`, `ServiceLoader` and149 configuration, and how it differs from Factory Method and Builder. Read before introducing or150 removing a factory interface.151- [Worked example](references/worked-example.md) — a report-export family selected per request,152 built first as a classical hierarchy and then as a record of suppliers, with the tenant-scoped153 variant, the failure path for an unknown format, and what each version costs. Read when154 implementing.