skip to content

How do interfaces plus implementing classes realize the 'deferred instantiation' intent of Factory Method, and how does this support open/closed extension of an API over time?

level: seniorimportance: should knowfreq 40%

answer

  1. Interface = contract; many implementers; factory returns the interface
  2. Deferred = which-class choice moved out of caller into factory
  3. OCP at the creation site: add a class, callers untouched
  4. ServiceLoader/SPI = runtime-discovered factory (plugins)
  5. Trade-off: indirection; don't abstract speculatively (YAGNI)

basics

~20 s

An interface says what an object does; classes implement it. A factory method returns the interface, so the caller only knows the interface. The factory can pick or add new implementing classes later without changing callers. That's deferred instantiation: the 'which class' decision is postponed and isolated inside the factory.

solid answer

~50 s

Deferred instantiation means the decision of *which concrete class to create* is postponed from the call site and centralized in the factory. Interfaces make this possible: the interface defines the contract (the behaviour callers rely on), implementing classes provide the concrete behaviours, and the factory method's return type is the interface. Callers compile against the interface and never reference an implementation, so they're unaffected when the factory chooses a different implementer or a brand-new one is added. This is the Open/Closed Principle at the creation site: the system is *open* to new implementations (add a class, teach the factory or SPI about it) but *closed* to modification of existing callers. The JDK leverages this through service-provider mechanisms — `ServiceLoader` / locale-service SPIs let new implementing classes be discovered at runtime, and factories like `NumberFormat.getInstance()` return whichever implementation is registered for the context. The trade-off is indirection: you lose visibility of the concrete type at the call site and must route through the factory.

go deeper

for a junior

Understands that the factory returns an interface so the caller doesn't see the concrete class, and a new implementation can be slotted in.

for a middle

Explains deferred instantiation as moving the which-class decision into the factory and can sketch swapping implementations without changing callers.

for a senior

Ties the interface+factory seam to the Open/Closed Principle and references ServiceLoader/SPI as the JDK's runtime-discovery form, while naming the indirection trade-off.

for a principal

Reasons about API and plugin evolution at scale — versioned SPIs, backward compatibility of returned types, when to expose a factory seam vs. keep it internal, and the cost of premature abstraction across a large codebase.

## The pieces: interface, implementation, factory - An **interface** in Java is a pure contract: a set of method signatures with no (or default) bodies. It states *what* an object can do. - An **implementing class** (`class Foo implements Bar`) supplies the *how* for that contract. There can be many implementers of one interface. - A **factory method** is a method whose *declared return type is the interface* but which *internally constructs a concrete implementer*. Because the caller holds only the interface reference, it is **decoupled** from every concrete class. The phrase **deferred instantiation** captures this: "instantiation" is the act of creating an object with `new`; "deferred" means that act — and especially the *choice of which class to instantiate* — is moved out of the caller and postponed to the factory, which can make the decision later and from richer context (configuration, locale, registered providers). ## A worked example of deferral ```java interface PaymentGateway { Receipt charge(Money m); } class StripeGateway implements PaymentGateway { /* ... */ } class PaypalGateway implements PaymentGateway { /* ... */ } class Gateways { static PaymentGateway forRegion(Region r) { // factory method return switch (r) { case US, CA -> new StripeGateway(); case EU -> new PaypalGateway(); }; } } // caller: depends only on the PaymentGateway interface PaymentGateway g = Gateways.forRegion(region); Receipt rcpt = g.charge(amount); ``` The calling code never names `StripeGateway` or `PaypalGateway`. The *which-class* decision lives entirely inside `Gateways.forRegion`. Adding `AdyenGateway` for a new region is a change *only* inside the factory (or a registry); no caller changes. The instantiation was "deferred" to the factory and is now trivially evolvable. ## How this realizes Open/Closed The **Open/Closed Principle (OCP)** says software entities should be *open for extension but closed for modification* — you should be able to add new behaviour without editing existing, tested code. Factory-method-over-interface delivers OCP *at the creation site*: - **Open for extension:** add a new class implementing the interface and make it reachable (register it, or extend the factory's mapping). - **Closed for modification:** existing callers, which depend only on the interface, are untouched. In the strongest form (a registry/SPI), even the factory needn't be edited. ## The JDK's runtime-discovery version: ServiceLoader / SPI The JDK pushes deferral to its limit with **SPI (Service Provider Interface)** and `java.util.ServiceLoader`. A *service* is an interface; *providers* are implementing classes declared in module metadata or `META-INF/services`. `ServiceLoader.load(Service.class)` discovers and instantiates whatever providers are on the module path at runtime. Here the set of concrete classes isn't even known when the consuming code is compiled — it's a factory whose products are *plugins*. JDBC drivers, `NumberFormat`/`Calendar` locale providers, and `Charset` providers all work this way. This is the ultimate deferred instantiation: the concrete class is chosen at runtime from a dynamically discovered, externally supplied set. ## Trade-offs and pitfalls - **Indirection cost.** You can no longer see the concrete type at the call site, which can hinder reading and debugging; navigation goes through the factory. - **Over-abstraction.** Introducing an interface + factory for a class with a single implementation and no extension need is speculative complexity (YAGNI). Add the seam when variation is real or clearly imminent. - **Discoverability.** Constructors are obvious; factory methods must be found and named well. - **Leaky returns.** If callers downcast the interface back to a concrete type, the decoupling is lost and OCP benefits evaporate — code to the interface. ## Interview takeaway State the chain crisply: *interface = contract callers depend on; implementations = interchangeable concretes; factory returns the interface and owns the which-class choice (deferred instantiation); this yields OCP at the creation site; the JDK's `ServiceLoader`/SPI is the runtime-discovery generalization.* Then name the trade-off (indirection / don't abstract speculatively) to show judgment.

  • What JDK mechanism takes deferred instantiation to runtime discovery of implementations?
    `java.util.ServiceLoader` with the Service Provider Interface (SPI): the service is an interface, providers are implementing classes registered in module metadata or `META-INF/services`, and `ServiceLoader.load(...)` discovers and instantiates them at runtime — e.g. JDBC drivers, charset and locale providers.
  • When is introducing a factory + interface premature?
    When there is exactly one implementation and no concrete, near-term need for another. The added indirection costs readability and navigation for no benefit (YAGNI); introduce the seam when a second implementation or a real extension point actually materializes.

saying these in an interview costs you the question

  • Claiming you must edit callers to introduce a new implementation — the whole point is you don't.
  • Downcasting the returned interface to a concrete type, which destroys the decoupling.
  • Adding an interface+factory for a single, never-varying implementation (speculative over-engineering).
  • Conflating compile-time factory selection with runtime SPI discovery — `ServiceLoader` finds providers at runtime.

context