Design Patterns in Java

Factory Method & Abstract Factory

15 min Lesson 3 of 13

Factory Method & Abstract Factory

Both patterns address the same root problem: client code should not need to know which concrete class it is instantiating. By hiding the new keyword behind a method or an object, you can swap implementations, add new variants, and test in isolation — all without touching the code that consumes the objects.

Why Object Creation Matters

When a class calls new ConcreteProduct() directly, it is tightly coupled to that implementation. Every place that coupling exists becomes a pain point when requirements change. The Factory patterns move that decision to one well-defined location, satisfying the Open/Closed Principle — open for extension, closed for modification.

Key distinction to hold in your head: Factory Method uses inheritance — a subclass decides what to create. Abstract Factory uses composition — an injected factory object creates families of related objects.

Factory Method

Define an interface for creating an object, but let subclasses decide which class to instantiate. The creator has a method — the "factory method" — declared abstract (or with a default) that subclasses override.

// Product interface public interface Notification { void send(String message); } // Concrete products public class EmailNotification implements Notification { @Override public void send(String message) { System.out.println("Email: " + message); } } public class SmsNotification implements Notification { @Override public void send(String message) { System.out.println("SMS: " + message); } } // Creator — declares the factory method public abstract class NotificationSender { // Factory method — subclasses override this protected abstract Notification createNotification(); // Template that uses the factory method public void notify(String message) { Notification n = createNotification(); n.send(message); } } // Concrete creators — one per variant public class EmailSender extends NotificationSender { @Override protected Notification createNotification() { return new EmailNotification(); } } public class SmsSender extends NotificationSender { @Override protected Notification createNotification() { return new SmsNotification(); } }

Client code works purely against NotificationSender. It never imports EmailNotification or SmsNotification:

NotificationSender sender = new EmailSender(); // or SmsSender — one line change sender.notify("Your order has shipped.");
When to choose Factory Method: You have a single product hierarchy, and you expect new variants to arrive over time. Each variant gets its own concrete creator class. Adding a PushNotification later means adding two classes — no existing code changes.

Abstract Factory

Abstract Factory goes one level further: it groups families of related products behind a single factory interface. Every concrete factory produces a full set of products that are guaranteed to be compatible with each other.

Consider a UI toolkit that must render on both a light theme and a dark theme. Each theme needs a matching Button and Checkbox. Mixing a dark Button with a light Checkbox would look broken — the factory prevents that mistake:

// Product interfaces public interface Button { void render(); } public interface Checkbox { void render(); } // Light-theme products public class LightButton implements Button { @Override public void render() { System.out.println("[ Light Button ]"); } } public class LightCheckbox implements Checkbox { @Override public void render() { System.out.println("☐ Light Checkbox"); } } // Dark-theme products public class DarkButton implements Button { @Override public void render() { System.out.println("[ Dark Button ]"); } } public class DarkCheckbox implements Checkbox { @Override public void render() { System.out.println("■ Dark Checkbox"); } } // Abstract Factory — one method per product type public interface ThemeFactory { Button createButton(); Checkbox createCheckbox(); } // Concrete factories — one per family public class LightThemeFactory implements ThemeFactory { @Override public Button createButton() { return new LightButton(); } @Override public Checkbox createCheckbox() { return new LightCheckbox(); } } public class DarkThemeFactory implements ThemeFactory { @Override public Button createButton() { return new DarkButton(); } @Override public Checkbox createCheckbox() { return new DarkCheckbox(); } }

The application class receives the factory via its constructor — classic dependency injection:

public class Application { private final Button button; private final Checkbox checkbox; public Application(ThemeFactory factory) { this.button = factory.createButton(); this.checkbox = factory.createCheckbox(); } public void renderUI() { button.render(); checkbox.render(); } } // Wiring — only this line decides the theme ThemeFactory factory = new DarkThemeFactory(); Application app = new Application(factory); app.renderUI();
Reading theme from config: In a real app you would read a property (e.g., theme=dark) from application.properties or an environment variable and instantiate the correct factory once in your bootstrap / DI container. Every downstream class stays unaware of the theme choice.

Comparing the Two Patterns

  • Factory Method — single product, variation via subclassing the creator. Fine for moderate numbers of variants.
  • Abstract Factory — multiple related products that must stay consistent; variation via swapping the whole factory object. Better for platform/theme/environment families.
  • Abstract Factory often uses Factory Methods internally (each create* method is effectively a factory method).

Static Factory Methods — a Lightweight Cousin

Java's own API uses static factory methods (e.g., List.of(), Optional.of(), Path.of()) as a simpler alternative when subclassing is unnecessary. They are not the GoF Factory Method pattern but share the intent of hiding construction:

public final class Money { private final long cents; private final String currency; private Money(long cents, String currency) { // private constructor this.cents = cents; this.currency = currency; } public static Money of(long cents, String currency) { if (cents < 0) throw new IllegalArgumentException("Negative money"); return new Money(cents, currency); } public static Money ofDollars(double amount) { return new Money(Math.round(amount * 100), "USD"); } } // Callers never write new Money(...) Money price = Money.ofDollars(9.99);
Common pitfall: Overusing factories for every single class adds unnecessary indirection. Apply the pattern when you genuinely expect multiple implementations or need to decouple the creation site from the construction details. If there is only ever one concrete class and that will not change, a direct new is simpler and clearer.

Trade-offs and Professional Notes

  • Factory patterns shine in framework and library design where the library cannot know which concrete class the consumer will provide.
  • In Spring applications, the IoC container is itself an Abstract Factory — @Bean methods are factory methods that Spring calls to build the application context.
  • Abstract Factory increases the number of interfaces and classes. Keep family boundaries small and well-named to avoid confusion.
  • Favour interface-based factory parameters (ThemeFactory factory) over concrete types — this keeps the constructor mockable in unit tests.

Summary

Factory Method decouples a creator from its concrete product by delegating instantiation to a subclass. Abstract Factory extends this to create families of related objects through a shared interface, ensuring all products within a family are compatible. Both patterns reduce coupling at the construction site and make your code open for new variants without modifying existing logic.