Factory Method Pattern
Learn how to hide the decision of which concrete class to instantiate behind one dedicated method, so callers can request an object by type without ever writing new on a concrete class themselves.
Learning objectives
- Recognize when scattered calls to construct concrete classes duplicate the same creation logic across a codebase
- Centralize object-creation decisions inside a single factory method
- Extend a factory to support a new type without modifying any existing calling code
- Judge when a Factory is worth the indirection, and when it is unnecessary overhead
◆ Story
Ordering "the pasta" at a restaurant does not require you to know which specific ingredients, cookware, or technique the kitchen uses — you just name what you want, and the kitchen decides how to actually produce it. If the kitchen later changes its recipe or its supplier, your order, "the pasta," never has to change.
That separation — naming what you want versus deciding how it gets built — is the core idea of the Factory pattern.
A ride-booking app needs the same separation. A TransportService lets a user request a car or a bike, and the service hands back the right kind of vehicle to fulfill that request. The obvious first version has TransportService construct whichever concrete vehicle class it needs, directly, inside its own methods. It is worth building that version first, and watching exactly what breaks when a third vehicle type needs to be supported.
The obvious way to fulfill a transport request is to give TransportService one method per vehicle type, and have each method construct the concrete class it needs directly: requestCar() calls new Car(), requestBike() calls new Bike().
This runs fine for two vehicle types — each method is short, and it is completely obvious what it does. The trouble starts the moment the product team wants to add buses. TransportService, the client code, is now tightly coupled to every concrete vehicle class it might ever need — it has to explicitly write new Car(), new Bike(), and eventually new Bus(), naming each concrete type directly inside its own code.
Adding a new vehicle means editing the client: supporting buses means going into already-written, already-tested TransportService code and adding a whole new method — a direct Open/Closed violation. And this scales badly across a real application: if ten different features each need to request a vehicle, the same "which class do I construct" decision ends up duplicated in ten different places, and a new vehicle type means finding and updating all ten of them correctly.
💻 Code example
interface Transport { void deliver(); } class Car implements Transport { public void deliver() { System.out.println("Delivering by car"); } } class Bike implements Transport { public void deliver() { System.out.println("Delivering by bike"); } } class TransportService { void requestCar() { Transport t = new Car(); // the client directly names the concrete class t.deliver(); } void requestBike() { Transport t = new Bike(); // and again here t.deliver(); } } // Fine for two vehicle types. What happens when the product team // wants to add buses -- and then trains, and then scooters?
TransportService's actual job is fulfilling transport requests — not deciding how to construct every possible vehicle class. Those are two separable concerns that ended up welded together. The "which concrete class to construct" decision belongs in exactly one place, so it is made consistently and only has to be updated once when a new type is added — and that one place should hand back a Transport, not a specific concrete class, so TransportService never needs to know or care which concrete type it actually received.
That is the Factory pattern: put all of that decision logic in exactly one place — a dedicated method or class whose entire job is creating the right concrete object — so calling code never has to know or repeat which concrete class to instantiate.
TransportFactory.createTransport(String type) is a static method, since there is no reason to first construct a TransportFactory object just to call a method that returns a Transport. Its switch statement is the single place in the entire codebase where "car" maps to new Car(), "bike" maps to new Bike(), and anything unrecognized throws an exception immediately, rather than returning something that fails confusingly later. With the factory in place, TransportService.requestTransport(type) shrinks to one line: ask the factory, then call deliver() on whatever it hands back. It never writes new on a concrete transport class again.
💻 Code example
class TransportFactory { static Transport createTransport(String type) { // static -- we want a Transport back, not a factory object switch (type.toLowerCase()) { // .toLowerCase() so "Car", "car", "CAR" all resolve the same way case "car": return new Car(); case "bike": return new Bike(); default: throw new IllegalArgumentException("Unsupported transport type: " + type); // fail LOUDLY } } } class TransportService { void requestTransport(String type) { Transport t = TransportFactory.createTransport(type); // ask the factory, never "new Car()" itself t.deliver(); } }
service.requestTransport("car") prints "Delivering by car" and service.requestTransport("bike") prints "Delivering by bike" — identical behavior to the naive version, just routed through the factory.
Now add buses, and look at exactly what changes. In the naive version, this meant opening TransportService and writing a new method. Here, it means writing one new class, Bus, implementing Transport, and adding exactly one new case to TransportFactory's switch statement. TransportService.requestTransport() is unchanged, byte for byte — the same Open/Closed proof point this pattern is built around, now applied specifically to the moment of object creation rather than to business logic in general.
💻 Code example
public class Main { public static void main(String[] args) { TransportService service = new TransportService(); service.requestTransport("car"); // "Delivering by car" service.requestTransport("bike"); // "Delivering by bike" } } // Adding bus support -- the entire diff: class Bus implements Transport { public void deliver() { System.out.println("Delivering by bus"); } } class TransportFactory { static Transport createTransport(String type) { switch (type.toLowerCase()) { case "car": return new Car(); case "bike": return new Bike(); case "bus": return new Bus(); // the only new line default: throw new IllegalArgumentException("Unsupported transport type: " + type); } } } // service.requestTransport("bus"); -- TransportService itself is NOT touched at all
▲ Edge case: an unrecognized type needs a loud, explicit failure
createTransport()'s default case throws immediately, with a clear message naming the bad type — rather than silently returning null. A caller that then called .deliver() on a null value would get a far less helpful NullPointerException, thrown somewhere completely unrelated to the actual mistake. Always fail loudly and specifically, as close to the actual bad input as possible.
▲ Edge case: a factory can grow into its own maintenance burden
As more types are added, createTransport()'s switch statement grows — which is fine, since it is now the only place that growth happens, but it is worth noticing this is structurally the same shape of conditional logic that Factory exists to fix in client code, just relocated somewhere sanctioned. For a large number of types, some codebases register creators in a map from type string to constructor function instead of a switch, avoiding the need to even edit the factory's method body for each new type.
▲ Common mistake: reaching for Factory when it is not needed
If you only ever create one concrete type from one place, a Factory is pure unnecessary indirection — reserve it for exactly the situation this chapter describes: which concrete class to create depends on some runtime condition, and that decision needs to happen consistently, in more than one place. Apply it where it demonstrably earns its keep, not reflexively everywhere object creation happens.
◆ Under the hood
Factory Method shows up anywhere client code needs an object of some family, and the specific concrete type should be decided by one central piece of logic rather than repeated everywhere.
java.util.Calendar.getInstance()— decides which concreteCalendarsubclass to return based on locale and time zone, without the caller ever naming a concrete class.java.text.NumberFormat.getCurrencyInstance()and its sibling static factory methods — return a concrete formatter implementation appropriate to the current locale, hidden behind one method call.- GUI frameworks — a button factory that returns a platform-specific button, styled for Windows, Mac, or Linux, depending on the operating system, without calling code needing to know which.
- Database connectivity — a factory that returns a SQL or NoSQL connection object based on configuration, so the code that requests a connection never names the specific driver class.
- Document conversion tools — requesting "a PDF generator" or "a Word doc generator" without the caller constructing the specific class itself.
Q: What problem does centralizing object creation in a Factory actually solve? : It stops the same "which concrete class to create" decision logic from being duplicated across many call sites, so adding a new type means editing exactly one place instead of finding every copy.
Q: Why is createTransport() a static method rather than an instance method? : Because the caller wants a Transport object back — there is no reason to first construct a TransportFactory object just to call a method that does not depend on any factory-specific state.
Q: When is a Factory genuinely unnecessary? : When only one concrete type is ever created from exactly one place — the pattern earns its value specifically when the decision is conditional and needed consistently in multiple places.
Q: Why does createTransport() throw an exception for an unrecognized type instead of returning null? : So the failure happens loudly and specifically at the actual point of the mistake, rather than surfacing later as a confusing NullPointerException wherever the null value eventually gets used.
Q: How does the Factory pattern relate to the Open/Closed Principle? : It is OCP applied specifically to object creation — adding a new type means adding a new class and one new case in the factory, with zero changes to any client code that requests objects through it.
Want a visual for this concept?
Generate a diagram tailored to “Factory Method Pattern” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →