Skip to main content
CleanCodeMastery

Replace Constructor with Factory Method: Order a Named Thali, Let the Kitchen Decide

Learn the Replace Constructor with Factory Method refactoring with a thali restaurant story, before/after TypeScript and C#, safe step-by-step migration, and a clear comparison with the full Factory Method design pattern.

26 min read Updated June 11, 2026beginner
refactoringfactory methodconstructorobject creationcreationaltypescriptcsharp

🍛 The thali counter problem

Come with me to Annapurna Bhavan, a busy lunch restaurant in Pune. The owner is Sunita tai, a no-nonsense lady who has run the place for twenty years. The head cook is Mohan, who can make dal that people cross the city for. And today a new customer walks in — Arjun, a first-year engineering student who has just shifted to the hostel nearby and is hungry enough to eat the menu board itself.

Except there is no menu board. In its early days, Annapurna Bhavan had a strange system. Every customer walked straight to the kitchen counter and assembled their own thali, item by item, following rules that were written nowhere.

Take a steel plate. Two rotis from that basket — the left basket, not the right one, the right one is for parcels. Dal from the second pot, not the first — the first is for staff. Rice, one scoop, using the flat ladle. Sabzi of the day. Pickle on the left edge of the plate, always the left. Papad balanced on top so it does not get soggy.

Arjun knows none of this. He takes three rotis and no rice. The uncle behind him takes dessert but no spoon. A school kid fills his entire plate with papad and nothing else, and pays full thali price for it. Mohan spends half his day shouting corrections across the counter: "Not that pot! One scoop only! Where is your rice?"

And here is the worse problem, the one Sunita tai loses sleep over. Whenever the kitchen changes anything — a new dal pot, new portion sizes, a price revision — every single customer has to relearn the steps. Two hundred customers, all carrying the recipe in their own heads, all slightly wrong in their own way.

One Monday, Sunita tai does one simple thing. She prints a menu with named thalis. "Veg Thali." "Special Thali." "Mini Thali." Now Arjun only says a name. Mohan's kitchen decides how to assemble it. The kitchen can swap today's sabzi, reuse pre-portioned dal cups during the lunch rush, and quietly serve a festival version on Diwali with kheer added — and no customer needs to know any of this. The name carries the intent; the freedom of preparation stays inside the kitchen.

In code, calling a raw constructor like new Meal(true, false, 2, "steel") is Arjun assembling the plate at the counter from a recipe he half-remembers. A named factory method like Meal.vegThali() is ordering from the menu. Today's refactoring, Replace Constructor with Factory Method, is the act of printing that menu.

Figure 1: Arjun's lunch before and after the menu was printed

Notice what the journey diagram is really saying. In the self-assembly days, both Arjun and Mohan are miserable, because the recipe lives in two hundred heads. In the menu days, both are happy, because the recipe lives in exactly one kitchen. Refactorings that make both the caller and the maintainer happier at the same time are rare and precious — this is one of them.

📜 What is Replace Constructor with Factory Method?

A constructor is a special method, and it comes with three hard limits that no mainstream language lets you escape.

  1. It has only one name — the class name. You cannot call one constructor openSavings and another openChecking. You can only pile up overloads, and overloads with similar parameter lists are a famous source of confusion. Quick — what does new Account(true) mean? Nobody knows without opening the class.
  2. It always returns exactly its own class. A Shape constructor can never hand back a Circle. It cannot return an interface. It cannot say "the kind depends on the input." The moment the caller writes new Shape(...), the decision is already made, at the wrong place.
  3. It always creates a fresh object. It cannot return a cached one, a pooled one, or a shared one. By the time the constructor body runs, memory has already been allocated. The decision "do we even need a new object?" was never available.

Replace Constructor with Factory Method says: write a normal static method whose job is creation. Inside, it calls the constructor (or one of several constructors, or a subclass constructor). Then point all callers at this method instead of at new. Finally, hide the constructor so the named method becomes the only door.

Martin Fowler describes this move in his classic Refactoring catalog — the second edition calls it Replace Constructor with Factory Function, because in JavaScript and similar languages the factory may be a free-standing function rather than a static method. The idea is identical: give creation a name and a brain.

💡

One-line summary: a constructor is a door with the class name written on it and no guard inside; a factory method is a named door with a guard who can check you, redirect you, or hand you something already prepared.

Because the factory is an ordinary method, it can do everything ordinary methods do. It can carry an intention-revealing name. It can validate inputs and refuse politely. It can pick which subclass to build. It can return an interface so callers never learn the concrete type. It can hand back a cached instance — refactoring.guru highlights exactly this: a factory method can return an already created object, while a constructor must always allocate a new one.

Here is the whole idea as one picture, before we dive into code.

Figure 2: Everything a factory method can do that a constructor cannot

And here is the same comparison as a table you can revise from. Keep it next to you during interviews — this exact question gets asked.

AbilityPlain constructorStatic factory method
Carry a descriptive nameNo — always the class nameYes — openSavings(), vegThali()
Return a subclassNo — always its own classYes — caller never knows which
Return an interface typeNoYes — hides the concrete class
Return a cached or pooled instanceNo — always allocatesYes — may return an existing object
Refuse to create (validate first)Only by throwing mid-constructionYes — check before any allocation
Be passed around as a function valueAwkward in most languagesYes — it is just a method reference
Be discovered automatically by DI containers and serializersYes — this is their defaultOften needs explicit registration

That last row is the honest one. The constructor's plainness is also its convenience: every framework on earth knows how to call it. We will return to this trade-off in the risks section.

🔍 When do we need it?

Watch for these signals in real code.

  • Constructor overloads that fight each other. new Account(true) vs new Account(false) — what do the booleans mean? When you wish you could name the variations, you want a factory: Account.openSavings(), Account.openChecking().
  • Callers choosing the subclass themselves. Code like kind === "circle" ? new Circle() : new Square() copy-pasted in five files. The creation decision belongs in one kitchen, not at every table.
  • You want to hide the concrete class. Callers should depend on a PaymentGateway interface, not on new RazorpayGateway(...). A constructor cannot return an interface; a factory can.
  • Creation must be controlled. Pooling database connections, caching flyweight objects, limiting instances, registering each new object somewhere — a constructor cannot do any of this honestly.
  • Validation before birth. If half-built objects must never exist, the factory checks first and throws a clear error instead of constructing garbage.

When Sunita tai's nephew (a CSE student, naturally) audited the billing codebase, he categorised every creation-related bug from the last year of code review comments. The result looked roughly like this — and it is typical of codebases that pass raw constructors around.

Figure 3: Where creation bugs came from in one year of code reviews

Every slice of that pie is a constructor limitation wearing a different costume. Wrong argument order happens because (2, true, false, 120) has no names. Copy-pasted recipes drift because the recipe lives at every call site. Invalid objects exist because nothing guards the door. Wrong subclasses get chosen because the caller was forced to decide something the kitchen should have decided.

🧭 The refactoring vs the full design pattern

Now, one very important clarification. This refactoring is related to, but smaller than, the Factory Method pattern from the Gang of Four book. Students mix these up constantly, so let us separate them cleanly.

  • The refactoring (this page) replaces new Thing(...) with Thing.create(...) — usually one static method on one class. It is a five-minute move with immediate benefits.
  • The full design pattern is an architecture. There is an abstract creator class with an abstract creation method, and subclasses of the creator override that method to decide which concrete product to build. It shines in frameworks, where the framework calls the creation method and your subclass supplies the product.

Think of it like this. The refactoring is printing the thali menu in one restaurant. The pattern is a restaurant chain where every branch follows the same menu format but each branch kitchen prepares its local version — Annapurna Bhavan Pune makes the Special Thali with paneer, the Chennai branch makes it with avial, and head office only ever talks about "the Special Thali" without knowing either recipe. Do the refactoring first. If, later, you find yourself switching on a type inside the factory again and again, the full pattern (or Abstract Factory) is the natural next stop. Fowler himself notes that this simple move is the gateway toward those bigger patterns.

⚖️ Before and after at a glance

Here is the restaurant's billing software before the refactoring. Customers — I mean callers — assemble meals by hand.

// BEFORE: every caller assembles the thali at the counter
class Meal {
  constructor(
    public rotis: number,
    public hasRice: boolean,
    public hasDessert: boolean,
    public price: number,
  ) {}
}
 
// Five files across the codebase do this, each slightly differently:
const lunch = new Meal(2, true, false, 120);   // "veg thali"... I think?
const special = new Meal(3, true, true, 180);  // hope the price is right
const broken = new Meal(0, false, false, 120); // an empty plate for 120!

Nothing stops the empty plate. Nothing explains what (2, true, false, 120) means. And if the Special Thali price changes, we must hunt through every file.

After the refactoring, the menu exists.

// AFTER: named factory methods — the kitchen decides the recipe
class Meal {
  private constructor(
    public rotis: number,
    public hasRice: boolean,
    public hasDessert: boolean,
    public price: number,
  ) {}
 
  static vegThali(): Meal {
    return new Meal(2, true, false, 120);
  }
 
  static specialThali(): Meal {
    return new Meal(3, true, true, 180);
  }
 
  static miniThali(): Meal {
    return new Meal(1, true, false, 80);
  }
}
 
const lunch = Meal.vegThali();        // reads like the menu
const special = Meal.specialThali();  // price lives in ONE place
// new Meal(0, false, false, 120)     -> compile error: constructor is private

Three wins at once. The call sites read like English. The recipe and price for each thali live in exactly one place. And the empty-plate bug is now impossible to write, because the constructor is private.

Figure 4: Before the menu, every caller carries the recipe; after, the factory holds it in one place

🪜 Step-by-step, the safe way

Like every good refactoring, we move in tiny steps and keep the program working after each one. Here is the route, following Fowler's mechanics.

Step 1: Create the factory method, delegating to the constructor. Do not change any caller yet. The new method simply wraps the old door.

class Meal {
  constructor( /* ...same as before, still public... */
    public rotis: number,
    public hasRice: boolean,
    public hasDessert: boolean,
    public price: number,
  ) {}
 
  // NEW: the factory just forwards for now
  static vegThali(): Meal {
    return new Meal(2, true, false, 120);
  }
}

The program compiles. Tests stay green. Nothing has moved yet — we only added a second door beside the first.

Step 2: Give the factory an intention-revealing name and the most abstract sensible return type. If callers only need an interface, return the interface. This is also the moment to decide names — vegThali() beats create1() forever.

Step 3: Find every new Meal(...) call and replace it, one at a time. Use your IDE's "find usages." After each replacement, compile and run the tests. If a call site does not match any existing factory, that is useful news — either add a new named factory or question whether that call site was correct in the first place. (Our empty-plate bug would be caught exactly here.)

Step 4: Move creation-time logic into the factory. Any validation, any subclass selection, any default-filling that callers were doing by hand now moves inside. The kitchen takes over the recipe.

Step 5: Make the constructor private (or protected, if subclasses need it). This is the lock on the old door. From now on, the named factories are the only way in.

Step 6: Run the full test suite, including a test for each named factory. Each thali deserves its own small test proving its recipe.

The migration has a clear shape — it is a small state machine, and the dangerous mistake is jumping straight from the first state to the last.

Figure 5: The safe migration path — the constructor gets locked only after every caller has moved
⚠️

Do not make the constructor private in step 1. If you lock the old door before all callers have moved to the new one, the whole codebase breaks at once and you will fix dozens of errors in a panic. The safe order is always: add the new door, migrate callers one by one with green tests in between, and lock the old door last. Also check your frameworks first — DI containers, serializers, and ORMs often need a reachable constructor, and a suddenly-private one can fail at runtime, not compile time.

After the refactoring, the runtime flow of a single order looks like this. The customer talks only to the factory; the constructor has become an internal kitchen tool.

Figure 6: After the refactoring, callers talk to the named factory and never to the constructor

🏪 A bigger real-life example

Let us grow the example to something with real decision-making. Annapurna Bhavan now takes online orders, and Sunita tai wants three behaviours that a plain constructor simply cannot give.

  1. On festival days, specialThali() should silently return the upgraded FestivalThali subclass with kheer added.
  2. The standard veg thali configuration should be shared (cached), not rebuilt for every one of the 400 lunch orders — Mohan pre-portions dal cups during the rush for exactly the same reason.
  3. No thali may ever be created with zero rotis — a validation rule in one place.

Here is the code that does all three. Notice how much intelligence now lives behind the named doors.

// The base class with a guarded, private constructor
class Thali {
  private static vegTemplate: Thali | null = null;
 
  protected constructor(
    readonly name: string,
    readonly rotis: number,
    readonly items: string[],
    readonly price: number,
  ) {
    if (rotis <= 0) {
      throw new Error(`A thali needs at least one roti, got ${rotis}`);
    }
  }
 
  // 1) Caching: the same immutable template serves every order
  static vegThali(): Thali {
    if (Thali.vegTemplate === null) {
      Thali.vegTemplate = new Thali(
        "Veg Thali", 2, ["dal", "rice", "sabzi", "pickle"], 120,
      );
    }
    return Thali.vegTemplate;
  }
 
  // 2) Subclass selection: the caller never knows which kind it got
  static specialThali(today: Date = new Date()): Thali {
    if (isFestival(today)) {
      return new FestivalThali();
    }
    return new Thali(
      "Special Thali", 3, ["dal", "rice", "paneer", "papad"], 180,
    );
  }
}
 
class FestivalThali extends Thali {
  constructor() {
    super("Festival Special", 3,
      ["dal", "rice", "paneer", "papad", "kheer"], 180);
  }
}
 
// Callers stay blissfully simple:
const order1 = Thali.vegThali();      // cached — no new allocation
const order2 = Thali.specialThali();  // maybe FestivalThali, maybe not

Try to achieve any one of these three behaviours with a raw public constructor. You cannot. A constructor must allocate a fresh object, must return its own class, and runs after the caller already committed to creating something. The factory method removes all three limits in one stroke.

The class structure now looks like a small family, with the factory methods as the only public doors.

Figure 7: The kitchen owns the recipes — factories on the base class, a hidden subclass for festivals

Read the arrows carefully: OrderScreen depends only on Thali. It has no arrow to FestivalThali at all. On Diwali, hundreds of customers receive kheer without a single line of ordering code knowing the subclass exists. That invisible arrow — the one that is not in the diagram — is the whole point.

💼 The same refactoring in C#

C# gives this refactoring extra polish. Static factory methods are a beloved idiom in .NET — think of TimeSpan.FromMinutes(5), Task.FromResult(x), or Guid.NewGuid(). All of them are named factories that the standard library chose over public constructors, for exactly the reasons on this page.

public class Account
{
    public string Id { get; }
    public decimal Balance { get; private set; }
    public decimal InterestRate { get; }
 
    // Private constructor: the only door is a named factory
    private Account(string id, decimal balance, decimal interestRate)
    {
        Id = id;
        Balance = balance;
        InterestRate = interestRate;
    }
 
    public static Account OpenSavings(string id) =>
        new Account(id, balance: 0m, interestRate: 0.04m);
 
    public static Account OpenChecking(string id) =>
        new Account(id, balance: 0m, interestRate: 0m);
 
    public static Account OpenFixedDeposit(string id, decimal amount)
    {
        if (amount < 1000m)
            throw new ArgumentException(
                "Fixed deposit needs a minimum of 1000.", nameof(amount));
        return new Account(id, amount, interestRate: 0.07m);
    }
}
 
// Call sites read like a bank form:
var savings = Account.OpenSavings("AC-101");
var fd = Account.OpenFixedDeposit("AC-102", 50_000m);

Compare Account.OpenFixedDeposit("AC-102", 50000) with new Account("AC-102", 50000, 0.07m, 2). The first one a class 7 student can read. The second one even its author will misread in three months.

One honest C# warning. Some .NET tooling expects constructors: the generic constraint where T : new() requires a public parameterless constructor, many serializers (and some ORMs) construct objects reflectively, and DI containers resolve services through constructors by default. None of these are blockers — serializers can be configured, and containers accept factory delegates like services.AddSingleton(sp => Account.OpenSavings("AC-1")) — but check before you lock the door.

🐍 And a quick look in Python

Python expresses the same idea with @classmethod, and the standard library is full of examples: dict.fromkeys(...), datetime.fromtimestamp(...), int.from_bytes(...). The convention of starting factory names with from or of makes the data source obvious.

class Thali:
    def __init__(self, name: str, rotis: int, price: int) -> None:
        if rotis <= 0:
            raise ValueError(f"A thali needs at least one roti, got {rotis}")
        self.name = name
        self.rotis = rotis
        self.price = price
 
    @classmethod
    def veg(cls) -> "Thali":
        return cls("Veg Thali", rotis=2, price=120)
 
    @classmethod
    def from_order_row(cls, row: dict) -> "Thali":
        # a factory that adapts a foreign shape into our class
        return cls(row["item_name"], int(row["roti_count"]), int(row["amount"]))
 
 
lunch = Thali.veg()
imported = Thali.from_order_row({"item_name": "Mini", "roti_count": "1", "amount": "80"})

Python cannot truly lock a constructor — there is no private keyword — but the team convention "always create through the classmethods" plus a leading-underscore constructor argument achieves the same discipline in practice. The from_order_row factory also shows a use we have not stressed yet: factories make wonderful adapters, converting messy external data into a clean object in one guarded place.

College corner: constructors vs static factories, the famous debate. If you study Java, this whole page is Item 1 of Joshua Bloch's Effective Java: "Consider static factory methods instead of constructors." Bloch lists advantages we have met — names, caching, subclass returns — plus one more that matters in API design: a static factory can return an object of a non-public class, so a library can expose an interface and keep every implementation class completely hidden (this is how Collections.unmodifiableList works — you receive a List and have no idea, and no need to know, which class it is). The standard naming vocabulary is worth memorising: of and from for conversions (List.of, Date.from), valueOf for boxed values, getInstance when the same instance may be reused, newInstance when a fresh one is guaranteed. Bloch is also honest about the costs: classes without public constructors cannot be subclassed by outsiders, and static factories are harder to find in documentation than constructors, which IDEs list automatically. That is why disciplined naming conventions exist — they make factories discoverable again. When your professor asks "why does Integer.valueOf(127) == Integer.valueOf(127) return true but new Integer(127) == new Integer(127) returned false in old Java" — the answer is this exact page: valueOf is a factory that returns cached objects for small values, while new was forced to allocate every time.

🤔 Should this class get a factory?

Not every class deserves a menu. A tiny data holder with one obvious recipe is better off with a plain constructor — printing a menu for a shop that sells only water is bureaucracy. Here is a map for the decision.

Figure 8: Where your class sits decides whether a factory method pays for itself

Bottom-left — a small DTO with one shape — keep new. Bottom-right — many meaningful variations but each is a plain fresh object — factories earn their keep purely through names. Top-left — one recipe but instances must be cached or pooled — the factory exists for return control. Top-right — many recipes and special returns, like our thali menu with caching and festival subclasses — the factory method is not optional there; it is the only honest design.

And what does the win look like in numbers? Sunita tai's nephew measured the simplest possible metric: when the Special Thali recipe changed (price revision in April), how many files needed editing?

Figure 9: One recipe change — files touched before and after the refactoring

Nine files down to one. That difference is the Shotgun Surgery smell being cured in real time — and it compounds with every future change.

🛠️ IDE support

This is one of the lucky refactorings with first-class automation in major IDEs.

  • IntelliJ IDEA (Java/Kotlin). Refactor → Replace Constructor with Factory Method does the whole dance for you: creates the static method, rewrites every constructor call across the project, and makes the constructor private — all in one safe, atomic operation. You choose the method name and even which class hosts the factory.
  • ReSharper and Rider (C#). Offer the same refactoring for .NET code, generating the static method and updating all usages.
  • Visual Studio (without ReSharper). No single-click version, but the combination of "Find All References", "Change Signature", and Quick Actions makes the manual route fast and safe.
  • TypeScript in VS Code. No automated refactoring exists, so follow the step-by-step section above by hand. "Find All References" on the constructor (place the cursor on the class name and search for new ClassName) gives you the migration checklist. The compiler becomes your safety net the moment you mark the constructor private — every missed caller turns into a red squiggle.

Even with automation, do the naming yourself. The IDE will happily call the method create, and create says nothing. OpenSavings says everything.

⚖️ Benefits and risks

Every refactoring is a trade. Here is the honest ledger.

BenefitsRisks / costs
Named creation expresses intent — openSavings() beats overloaded constructorsExtra indirection; for a simple class with one obvious recipe, plain new is clearer
Can return subclasses or interface types, hiding concrete classes from callersSome DI containers, serializers, and new() generic constraints expect a public constructor
Can return cached or pooled instances instead of always allocatingCaching adds shared state — cached objects must be immutable or trouble follows
Validation and creation rules live in one place, impossible to skipA crowd of factory methods can make it harder to discover "how do I build one of these?"
Natural first step toward the full Factory Method and Abstract Factory patternsThe constructor must actually be made non-public, or callers will quietly bypass the factory
Call sites become testable seams — tests can read the named intentSlightly more code: one method per named variation

A fair rule of thumb: if you cannot name even one concrete benefit from the left column for your class, keep the constructor. The menu is for restaurants with more than one dish.

🧹 Which smells does it cure?

SmellHow this refactoring helps
Switch StatementsA creation switch copy-pasted across callers collapses into one factory; later it can dissolve into polymorphism
Large ClassTangled constructor overloads and creation flags move out into clearly named factory methods
Long Parameter ListNamed factories with sensible defaults replace constructors taking six mysterious arguments
Duplicate CodeThe same "assemble the object correctly" dance repeated at many call sites funnels into one place
Shotgun SurgeryChanging a recipe (price, defaults, subclass choice) touches one factory, not every caller

📋 Quick revision box

+------------------------------------------------------------------+
|   REPLACE CONSTRUCTOR WITH FACTORY METHOD - REVISION CARD        |
+------------------------------------------------------------------+
| Problem  : constructors cannot be named, cannot return another   |
|            type, and always allocate a fresh object              |
| Solution : static factory method wraps the constructor;          |
|            then make the constructor private                     |
|                                                                  |
| THE FACTORY CAN (constructor cannot):                            |
|   - carry a meaningful name      (Account.OpenSavings)           |
|   - return a subclass/interface  (kitchen decides the thali)     |
|   - return a cached instance     (no fresh plate every time)     |
|   - validate and refuse          (no zero-roti thali)            |
|                                                                  |
| SAFE ORDER : add factory -> migrate callers one by one           |
|              -> lock constructor LAST                            |
| Remember   : refactoring = one named method;                     |
|              full GoF pattern = subclass overrides creation      |
+------------------------------------------------------------------+

✏️ Practice exercise

Your turn. A travel app creates tickets like this, and the call sites are full of mystery booleans.

class Ticket {
  constructor(
    public train: string,
    public isAC: boolean,
    public isTatkal: boolean,
    public fare: number,
  ) {}
}
 
// scattered across the codebase:
const t1 = new Ticket("12952 Rajdhani", true, false, 3200);
const t2 = new Ticket("12952 Rajdhani", true, true, 3700);  // tatkal: +500
const t3 = new Ticket("11077 Jhelum", false, true, 950);    // is this fare right?

Do the refactoring yourself, step by step:

  1. Add three static factories without touching any caller: Ticket.acTicket(train, fare), Ticket.tatkalTicket(train, baseFare) (it should add the ₹500 tatkal charge inside the factory), and Ticket.sleeperTicket(train, fare).
  2. Migrate the three call sites one at a time, compiling after each.
  3. Make the constructor private and confirm the compiler now rejects any direct new Ticket(...).
  4. Add a validation rule in one place: no ticket may have a fare below ₹50.
  5. Bonus thinking: the app now wants Ticket.tatkalTicket to return a TatkalTicket subclass with a bookingWindow property. Which limit of constructors does this exercise demonstrate? Write one sentence.
  6. Stretch goal: describe (in words, no code) what would have to change if ticket families — train, bus, and flight tickets each with AC/non-AC variants — needed creating together. Which pattern from the creational patterns family is calling you?
  7. Diagram practice: sketch the Figure 5 state machine for your migration of Ticket, marking which state you are in after each of steps 1 to 3.

If you can explain to a friend why the ₹500 tatkal charge belongs inside the factory and not at the call sites — the way Arjun can now explain why the kheer decision belongs to Mohan's kitchen and not to the customer — you have understood this refactoring completely.

Frequently asked questions

How is this refactoring different from the Factory Method design pattern?
The refactoring is one small move — wrap a constructor call inside a named static method on the same class. The full design pattern is bigger — a hierarchy of creator classes where subclasses override a creation method to decide which product to build. The refactoring is often the first step that later grows into the pattern, but most of the time the simple static method is all you need.
Should every constructor be replaced with a factory method?
No. For a small class with one obvious way to build it, the plain constructor is shorter and clearer. Reach for a factory method only when you need a meaningful name, a different return type, a subclass decision, caching, or validation before creation.
Why do we make the constructor private at the end?
If the constructor stays public, callers can still bypass the factory and create objects the raw way. Making it private forces every creation through the single named door, so the rules inside the factory can never be skipped.
Can a factory method return something other than a new object?
Yes, and that is its superpower. It can return a subclass, an interface type that hides the concrete class, a cached or pooled instance instead of a fresh one, or it can refuse to create anything and fail with a clear error.
Do factory methods cause problems with frameworks?
Sometimes. Many DI containers, serializers, and generic constraints expect a public constructor and do not discover static factory methods automatically. Check your framework before locking the constructor down, or register the factory explicitly with the container.

Further reading

Related Lessons