Java

Factory Method Pattern in Java with a Document Export Example

4 min read Updated Mar 27, 2026

Java Design Patterns Series

Factory Method solves a specific problem: object creation varies, but the surrounding workflow stays stable. That is common in backend systems that support multiple output formats or providers behind the same use-case flow.


Problem 1: Stable Export Workflow with Variable Exporter Creation

Problem description: We want to export a sales report as CSV, JSON, or PDF. Validation, orchestration, and audit logging should remain unchanged.

What we are solving actually: We are solving for variation in one creation choice without duplicating the surrounding use-case flow. If every caller decides exporter type on its own, the export workflow spreads across the codebase and becomes inconsistent.

What we are doing actually:

  1. Keep the export workflow in one place.
  2. Move the product-selection decision into a factory method.
  3. Let new export types extend creation logic without rewriting validation and audit behavior.

The pressure here is not “how do I instantiate an object?” The pressure is “how do I keep one stable export workflow while allowing one creation choice to vary?”


UML

classDiagram
    class ReportExporter {
      <<interface>>
      +export()
    }
    class ExportService {
      +run()
      -createExporter()
    }
    ReportExporter <|.. CsvReportExporter
    ReportExporter <|.. JsonReportExporter
    ReportExporter <|.. PdfReportExporter
    ExportService --> ReportExporter

Implementation Walkthrough

public enum ExportType {
    CSV, JSON, PDF
}

public interface ReportExporter {
    String export(SalesReport report);
}

public final class CsvReportExporter implements ReportExporter {
    @Override
    public String export(SalesReport report) {
        return "date,total\n" + report.getDate() + "," + report.getTotalRevenue();
    }
}

public final class JsonReportExporter implements ReportExporter {
    @Override
    public String export(SalesReport report) {
        return "{\"date\":\"" + report.getDate() + "\",\"total\":" + report.getTotalRevenue() + "}";
    }
}

public final class PdfReportExporter implements ReportExporter {
    @Override
    public String export(SalesReport report) {
        return "PDF-BINARY(" + report.getDate() + "," + report.getTotalRevenue() + ")";
    }
}

public class ExportService {
    public String run(SalesReport report, ExportType type) {
        validate(report);
        ReportExporter exporter = createExporter(type); // Only this creation decision varies.
        String payload = exporter.export(report);
        audit(type, report);
        return payload;
    }

    protected ReportExporter createExporter(ExportType type) {
        switch (type) {
            case CSV:
                return new CsvReportExporter();
            case JSON:
                return new JsonReportExporter();
            case PDF:
                return new PdfReportExporter();
            default:
                throw new IllegalArgumentException("Unsupported type: " + type);
        }
    }

    private void validate(SalesReport report) {
        if (report == null) {
            throw new IllegalArgumentException("Report is required");
        }
    }

    private void audit(ExportType type, SalesReport report) {
        System.out.println("Exported " + type + " for " + report.getDate());
    }
}

Usage:

ExportService service = new ExportService();
String result = service.run(new SalesReport("2025-12-04", 125000), ExportType.JSON);

The example keeps exporter creation inside the service because that is the part that varies while validation and audit behavior stay fixed. If a later requirement adds XML, the change is localized to the creation decision instead of being repeated in every caller that needs export support.


Why This Is Better Than Inline Conditionals

The creation logic has one responsibility: choose an exporter. The workflow logic has another responsibility: validate, export, audit.

If later we add XML, we touch creation logic only. That is the exact pressure Factory Method is meant to absorb.

This is the real payoff: not fewer lines in one class, but fewer decision points across the codebase.


Debug Steps

Debug steps:

  • verify every supported ExportType resolves to exactly one exporter
  • add tests that new exporter types do not change validation or audit sequencing
  • inspect callers to ensure they do not branch again after factory creation
  • check whether the problem has grown into a multi-object family, which may signal Abstract Factory instead

Where It Starts to Break Down

If the family of created objects expands beyond one type, Factory Method may become too narrow. For example, if each region needs:

  • an exporter
  • a validator
  • a formatter

then Abstract Factory becomes a better fit.


Key Takeaway

Use Factory Method when the workflow is fixed but the product object changes. Do not use it if a simple constructor already communicates the intent clearly.

Categories

Tags

Continue reading

Previous Singleton Pattern in Java with a Configuration Registry Example Next Abstract Factory Pattern in Java with Region-Aware Service Families

Comments