The line new EmailSender() sits deep inside a business class and looks harmless, until the day a customer asks for SMS, then Zalo, and your test suite can't run without an SMTP server. Factory Method and Abstract Factory are two creational design patterns built for exactly this situation: they separate "what gets created" from "what the code does with it." This article implements both in an order notification and payment system, compiled with --release 17 (the Java 17 LTS language and API level), with real output pasted in. It also compares them with static factory methods in the JDK and says plainly when these patterns only make code more cumbersome.
This article continues Singleton pattern in Java: lazy init, holder, enum and the Java Memory Model in the same series, and reuses the dependency-inversion mindset from SOLID principles in Java with practical examples.
The problem: new buried in business logic
Picture a class that sends an "order shipped" notification to a customer. The naive version creates the sender right inside the method:
class OrderNotifier {
void orderShipped(String customer, String orderId) {
EmailSender s = new EmailSender(); // tightly bound to one concrete class
s.send(customer, "Order " + orderId + " shipped");
}
}This has three consequences. First, adding an SMS channel means editing this very class, which violates the Open-Closed principle. Second, the business class depends on a concrete class, against Dependency Inversion. Third, a unit test cannot replace EmailSender with a fake. Factory Method solves all three with one small change: move the new statement into a separate method that subclasses can override.
Factory Method: structure and implementation
According to the Refactoring.Guru description, Factory Method defines a method for creating objects in a superclass and lets subclasses decide which concrete type to create. The pattern has four roles: Product is the common interface of all products, Concrete Product is each implementation, Creator declares the factory method that returns the Product type (and also holds the business logic that uses the product), and Concrete Creator overrides the factory method to return a specific product. A commonly misunderstood point: the Creator's main job is not creating objects but the business logic built on the Product; the factory method is only the "hook" that lets subclasses plug in a product.
Applied to the example, Sender is the Product and OrderNotifier is the Creator with the factory method createSender():
interface Sender { void send(String to, String text); }
static class EmailSender implements Sender {
public void send(String to, String text) { System.out.println("[EMAIL] " + to + " <- " + text); }
}
static class SmsSender implements Sender {
public void send(String to, String text) {
String cut = text.length() > 40 ? text.substring(0, 37) + "..." : text;
System.out.println("[SMS] " + to + " <- " + cut);
}
}
static abstract class OrderNotifier {
protected abstract Sender createSender(); // factory method
void orderShipped(String customer, String orderId) {
Sender s = createSender();
s.send(customer, "Order " + orderId + " has been handed to the shipping carrier");
}
}
static class EmailOrderNotifier extends OrderNotifier {
protected Sender createSender() { return new EmailSender(); }
}
static class SmsOrderNotifier extends OrderNotifier {
protected Sender createSender() { return new SmsSender(); }
}Add one more Concrete Creator for the test environment that records messages instead of sending them, without changing a single line of OrderNotifier:
static class RecordingSender implements Sender {
final List<String> sent = new ArrayList<>();
public void send(String to, String text) { sent.add(to + "|" + text); }
}
static class TestNotifier extends OrderNotifier {
final RecordingSender rec = new RecordingSender();
protected Sender createSender() { return rec; }
}=== java -cp out FactoryMethodDemo
[EMAIL] an@example.com <- Order DH-2001 has been handed to the shipping carrier
[SMS] 0900000000 <- Order DH-2001 has been handed to the ...
[TEST] recorded 1 message: test@example.com|Order DH-9 has been handed to the shipping carrierThe third line shows the direct benefit for testing: the notification logic is verified without a network. Note that Factory Method relies on inheritance: each variant is a subclass of the Creator. As the number of variants grows, so does the number of subclasses, which is the main drawback that Refactoring.Guru also points out.
Abstract Factory: creating a whole family of objects that belong together
Factory Method creates one kind of product. Abstract Factory creates a family of related products and guarantees they stay compatible. A realistic example: a payment gateway integration with two environments, sandbox and live. Each environment needs an API client and a webhook signature verifier, and mixing a sandbox client with a live verifier leads to bugs that are hard to find. Abstract Factory forces the two to travel together:
interface PaymentClient { String charge(String orderId, long amount); }
interface WebhookVerifier { boolean verify(String payload, String signature); }
interface PaymentKit { // Abstract Factory
PaymentClient client();
WebhookVerifier verifier();
}
static class SandboxKit implements PaymentKit {
public PaymentClient client() { return new SandboxClient(); }
public WebhookVerifier verifier() { return new SandboxVerifier(); }
}
static class LiveKit implements PaymentKit {
public PaymentClient client() { return new LiveClient(); }
public WebhookVerifier verifier() { return new LiveVerifier(); }
}
static PaymentKit kitFor(String env) {
return switch (env) {
case "live" -> new LiveKit();
case "sandbox" -> new SandboxKit();
default -> throw new IllegalArgumentException("Unsupported environment: " + env);
};
}
static void handle(PaymentKit kit, String payload, String sig) {
System.out.println(kit.client().charge("DH-3001", 450_000)
+ " | webhook valid = " + kit.verifier().verify(payload, sig));
}This example makes no real network calls: the clients are simulated so the focus stays on structure. The handle function only knows PaymentKit, not which family is in use:
=== java -cp out AbstractFactoryDemo
sandbox: SANDBOX-OK DH-3001 450000 | webhook valid = true
live: LIVE-OK DH-3001 450000 | webhook valid = false
ERROR: Unsupported environment: staging
With the same fake signature, sandbox accepts and live rejects: exactly the desired behavior, and switching environments is just changing one argument to kitFor. An unknown environment name such as staging is rejected immediately, instead of silently falling back to a dangerous default.
Factory Method vs Abstract Factory
| Criterion | Factory Method | Abstract Factory |
|---|---|---|
| What it creates | One product | A family of related products |
| Mechanism | Inheritance: a subclass overrides a method | Composition: pass a factory object in |
| Adding a new variant | Add one Creator subclass | Add one concrete factory covering the whole family |
| Adding a new product type to the family | Not applicable | Must change the factory interface and every implementation (expensive) |
| Complexity | Low | Higher |
Refactoring.Guru notes a common evolution path: start with Factory Method (simple, extended through subclasses), and move to Abstract Factory when you begin to need several related products that must stay consistent. Abstract Factory itself is often built from a set of Factory Methods. In the example above, the client() and verifier() methods are exactly two factory methods sharing one interface.
Static factory method: the simplest and most common variant in the JDK
Many real-world "factories" have no subclass hierarchy at all and are just a static method that returns an interface. This is the first item in Joshua Bloch's Effective Java: consider static factory methods instead of constructors, because they have meaningful names, can return a cached instance or a subtype, and are not required to create a new object. It is often combined with a registry so that a new variant takes one registration line instead of editing a switch:
interface Shipper { String quote(int kg); }
record Standard() implements Shipper { public String quote(int kg) { return "Standard " + (20_000 + 5_000L * kg); } }
record Express() implements Shipper { public String quote(int kg) { return "Express " + (45_000 + 9_000L * kg); } }
static final Map<String, Supplier<Shipper>> REGISTRY = Map.of(
"standard", Standard::new,
"express", Express::new);
static Shipper of(String code) {
Supplier<Shipper> s = REGISTRY.get(code);
if (s == null) throw new IllegalArgumentException("Unknown carrier: " + code);
return s.get();
}=== java -cp out StaticFactoryDemo
Standard 35000
Express 72000
ERROR: Unknown carrier: drone
The JDK itself uses this approach everywhere. I ran a few common factories to see the actual types returned:
=== java -cp out JdkFactories
Calendar.getInstance() -> java.util.GregorianCalendar
NumberFormat vi-VN -> java.text.DecimalFormat : 1.234.567,89
List.of(1) -> java.util.ImmutableCollections$List12
List.of(1,2,3) -> java.util.ImmutableCollections$ListN
Integer.valueOf(127) same -> true
Integer.valueOf(1000) same -> false
Charset.forName("UTF-8") -> sun.nio.cs.UTF_8
DocumentBuilderFactory -> com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl
List.of(...).add -> UnsupportedOperationExceptionThese lines illustrate exactly the reasons to use a factory. Calendar is an abstract class, and the Calendar documentation describes getInstance as a factory that returns a subclass suited to the locale and time zone, here GregorianCalendar. List.of picks a different implementation class depending on the element count (List12 for 1 to 2 elements, ListN for more), something a constructor cannot do. Integer.valueOf reuses objects in the range -128 to 127, so two calls with 127 return the same object while 1000 does not, which is why comparing Integer values with == is a classic bug.
For XML, DocumentBuilderFactory.newInstance() is a factory in the spirit of JAXP's Abstract Factory (academically closer to a factory that locates an implementation through a service provider than to a product family): it returns an XML implementation that you can swap through configuration without changing code.
Factories in Spring
In Spring, the container itself is one giant factory: BeanFactory creates and manages beans according to their definitions. The Spring documentation also supports declaring beans created by a static factory method or an instance factory method, and provides the FactoryBean interface for objects with complex creation logic.
Note how the Spring 5.3 documentation distinguishes two concepts: a "factory bean" is a bean that creates another object through a factory method, while FactoryBean (capitalized) is a Spring-specific interface. With Spring, the need to hand-write a Factory Method drops a lot because the container does the "which implementation" selection through configuration, profiles or @ConditionalOnProperty; you only write a factory yourself when the type to create depends on runtime data, like the carrier code in the example above.
When not to use Factory Method
- Only one implementation and no need to swap it in tests: a factory class that merely wraps
newis cost without benefit. Calling the constructor directly is clearer. - You already have a DI container: let the container inject the right implementation through the constructor instead of writing a Creator tree yourself.
- Many independent variants: the subclass tree explodes. Switch to a registry or pass a
Supplieras in the example above, instead of one subclass per variant. - A product family whose structure changes often: Abstract Factory is rigid because adding a new product type forces changes to every concrete factory. Use it only when the product family is stable while new variants (environments, platforms) appear often.
Common mistakes
- Calling
newinside the Creator itself, outside the factory method: this defeats the purpose of decoupling. Look for every productnewduring review. - A factory that returns
nullfor an unknown argument: the failure surfaces far from its cause. ThrowIllegalArgumentExceptionwith a clear message, as in thekitFor("staging")example. - Calling a factory method from a superclass constructor: the subclass is not fully initialized yet, so its fields are still
nullor 0. Avoid calling overridable methods in a constructor. - A factory that both creates and holds business logic: this mixes two responsibilities and violates SRP. A factory should only decide what to create.
- Confusing it with Simple Factory (one class with a
switchthat returns products): useful, but not the GoF Factory Method because it does not use inheritance to choose the type. Naming it correctly helps teammates understand the design intent. - Mixing products from two families when assembling by hand instead of going through an Abstract Factory: your tests need a case that catches this error.
Checklist and summary
- Does business logic call
newon a class with many variants or one that must be replaced in tests? - One product: use Factory Method; a family of products that must stay consistent: use Abstract Factory.
- Variants chosen from runtime data: use a static factory method or a
Map<String, Supplier<T>>registry. - Already on Spring: prefer letting the container choose the implementation through configuration before writing a factory yourself.
- Every unknown argument must produce a clear error, never return
null.
The full source (FactoryMethodDemo, AbstractFactoryDemo, StaticFactoryDemo, JdkFactories) compiles with javac --release 17 -d out *.java and runs with java -cp out <Name> at the Java 17 (LTS) API level, on a 16 GB RAM machine running macOS. Internal JDK class names (such as sun.nio.cs.UTF_8) can differ between JDK builds, so don't depend on them in your own code.
Next article
Keep reading: Builder pattern: from Effective Java to Lombok and records.



