Skip to content

Low Level DesignDesign patterns

Factory pattern

Factory method, abstract factory and registry factories through three real examples, from notification channels to payment providers to database clients, with diagrams, live demos and code in five languages.

Foundation20 min read

A factory is an object or method whose job is to create other objects. Then the code that uses them doesn’t have to. The caller says what it needs: “a notifier for SMS”, “payments for the EU”, “a client for this database URL”. It gets back something behind an interface. Which class that is, how it’s configured, and where it comes from are decided in one place.

It’s the most common pattern in real code, often without the name: Logger.getLogger, List.of, http.NewRequest and every dependency injection container are factories. This page builds from a simple registry to a factory that owns the lifecycle of what it creates.

The idea in one picture
OrderService needs "a notifier" create("sms") Factory the only place that says new "email" → EmailNotifier "sms" → SmsNotifier "push" → PushNotifier + register(...) at startup EmailNotifier SmsNotifier PushNotifier builds a Notifier never sees SmsNotifier, its settings or its imports a new channel = one new box
Scroll or pinch to zoom, drag to pan
The caller asks by name and gets an interface back. Only the factory knows the concrete classes, their settings and their imports.

Every factory has the same parts:

  • The product interface is what callers use, for example Notifier.
  • Concrete products implement it, for example EmailNotifier and SmsNotifier.
  • The factory decides which concrete product to create and how. Callers depend on it and on the interface, never on the concrete products.
The general shape

asks

uses

creates

creates

Client

Factory

+create(kind) : Product

«interface»

Product

+use()

ProductA

ProductB

asks

uses

creates

creates

Client

Factory

+create(kind) : Product

«interface»

Product

+use()

ProductA

ProductB

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation

The client has no arrow to ProductA or ProductB. That missing arrow is the whole point: adding ProductC touches the factory and nothing that uses it.

The three forms differ in who decides what gets created:

Form Who decides Use it when Example on this page
Simple factory or registry A function or object, by a name or config value The choice comes from data: a setting, a request, a URL Easy, Hard
Factory method A subclass, by overriding one creation method A base class runs an algorithm and one step needs a different object per subclass Below
Abstract factory One object with a creation method per part Several parts must come from the same family Medium

The original Gang of Four pattern. A base class runs the shared algorithm and calls an overridable method to create the one object that varies. Each subclass returns its own:

Campaign.javacomposed_campaign.pyCampaign.cscampaign.gocampaign.ts
import java.util.List;
import java.util.function.Supplier;
interface Notifier { void send(String to, String text); }
class EmailNotifier implements Notifier {
public void send(String to, String text) { System.out.println("email to " + to + ": " + text); }
}
class SmsNotifier implements Notifier {
public void send(String to, String text) { System.out.println("sms to " + to + ": " + text); }
}
// Factory method: the base class runs the algorithm and leaves one decision, which object
// to create, to a method that subclasses override. The base never names a concrete class.
abstract class Campaign {
// The algorithm every campaign shares. It calls the factory method instead of `new`.
final void run(List<String> users, String text) {
Notifier notifier = createNotifier();
for (String user : users) notifier.send(user, text);
}
// The factory method. Each subclass decides which Notifier its campaign uses.
protected abstract Notifier createNotifier();
}
class EmailCampaign extends Campaign {
protected Notifier createNotifier() { return new EmailNotifier(); }
}
class SmsCampaign extends Campaign {
protected Notifier createNotifier() { return new SmsNotifier(); }
}
// The same flexibility by composition: pass the factory in instead of subclassing.
// Usually the better choice today: no class per variant, and it can change at run time.
// Factory method still fits when a framework owns the base class (see the page).
class ComposedCampaign {
private final Supplier<Notifier> notifiers;
ComposedCampaign(Supplier<Notifier> notifiers) { this.notifiers = notifiers; }
void run(List<String> users, String text) {
Notifier notifier = notifiers.get();
for (String user : users) notifier.send(user, text);
}
}
class Main {
public static void main(String[] args) {
new EmailCampaign().run(List.of("ana", "ben"), "sale starts today");
new ComposedCampaign(SmsNotifier::new).run(List.of("ana"), "sale ends tonight");
}
}

Today it’s mostly found inside frameworks. The framework owns the base class, and you override the hook (onCreateView in Android, get_queryset in Django). In your own code, there’s a simpler way. Pass a function that creates the object (the ComposedCampaign version). It does the same job, without a class per variant. Go has no inheritance, so that’s the only way it can do it.

Easy

Notification channels

  • Creation in one place
  • Pick by name
  • A registry instead of a switch

An online shop sends messages by email and SMS. Order confirmations, shipping updates and on-call alerts all need “a notifier for channel X”. Each channel has its own settings: an SMTP host, an SMS API key. Push notifications are coming next quarter, then Slack.

Each service builds its own notifier:

OrderService.javaalert_service.pyOrderService.csr.goalert-service.ts
// Before: every caller decides which concrete class to build, and how.
interface Notifier { void send(String to, String text); }
class EmailNotifier implements Notifier {
private final String smtpHost;
EmailNotifier(String smtpHost) { this.smtpHost = smtpHost; }
public void send(String to, String text) { System.out.println("email via " + smtpHost + " to " + to + ": " + text); }
}
class SmsNotifier implements Notifier {
private final String apiKey;
SmsNotifier(String apiKey) { this.apiKey = apiKey; }
public void send(String to, String text) { System.out.println("sms to " + to + ": " + text); }
}
class OrderService {
void confirm(String channel, String user) {
// flaw 1: the same if/else, with the same settings, is copied into every caller.
// flaw 2: OrderService now depends on every concrete notifier and its constructor.
Notifier n;
if (channel.equals("email")) n = new EmailNotifier("smtp.internal");
else if (channel.equals("sms")) n = new SmsNotifier("sms-key");
else throw new IllegalArgumentException(channel);
n.send(user, "order confirmed");
}
}
class AlertService {
void page(String channel, String user) {
// flaw 3: adding push notifications means finding and editing every copy.
Notifier n;
if (channel.equals("email")) n = new EmailNotifier("smtp.internal");
else if (channel.equals("sms")) n = new SmsNotifier("sms-key");
else throw new IllegalArgumentException(channel);
n.send(user, "disk almost full");
}
}
class Main {
public static void main(String[] args) {
new OrderService().confirm("email", "ana"); // email via smtp.internal to ana: order confirmed
new AlertService().page("sms", "ops"); // sms to ops: disk almost full
}
}

The same branch, with the same settings, is copied into every service. Each one imports every notifier class. Adding push means finding every copy, and the one that gets missed fails only when a customer picks push.

Move creation into one object. It maps a channel name to a function that builds that channel’s notifier, and services ask it by name:

Easy: one registry, any number of channels

asks by name

uses

creates

OrderService

+confirm(channel, user)

NotifierFactory

-makers: Map<String, Supplier>

+register(name, maker)

+create(name) : Notifier

«interface»

Notifier

+send(to, text)

EmailNotifier

-smtpHost

SmsNotifier

-apiKey

asks by name

uses

creates

OrderService

+confirm(channel, user)

NotifierFactory

-makers: Map<String, Supplier>

+register(name, maker)

+create(name) : Notifier

«interface»

Notifier

+send(to, text)

EmailNotifier

-smtpHost

SmsNotifier

-apiKey

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation

Register a channel, then ask the service to use it:

Startup code (composition root)

factory.register("email", ...);
factory.register("sms", ...);

Registry

  • email
  • sms

OrderService 0 edits

Pick a channel and confirm. Then register a new one and try again.

NotifierFactory.javaorder_service.pyNotifierFactory.csnotifier_factory.gonotifier-factory.ts
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
interface Notifier { void send(String to, String text); }
class EmailNotifier implements Notifier {
private final String smtpHost;
EmailNotifier(String smtpHost) { this.smtpHost = smtpHost; }
public void send(String to, String text) { System.out.println("email via " + smtpHost + " to " + to + ": " + text); }
}
class SmsNotifier implements Notifier {
private final String apiKey;
SmsNotifier(String apiKey) { this.apiKey = apiKey; }
public void send(String to, String text) { System.out.println("sms to " + to + ": " + text); }
}
// A registry factory: one place that knows names, concrete classes and their settings.
// Callers ask for "email" and get a Notifier; they never see a constructor.
class NotifierFactory {
// Name -> how to build one. A Supplier instead of a switch, so a new channel is a
// register() call at startup, not an edit to this class.
private final Map<String, Supplier<Notifier>> makers = new HashMap<>();
void register(String name, Supplier<Notifier> maker) {
// Fail loudly on duplicates: two modules silently fighting over "sms" is a bug.
if (makers.putIfAbsent(name, maker) != null)
throw new IllegalStateException("already registered: " + name);
}
Notifier create(String name) {
Supplier<Notifier> maker = makers.get(name);
if (maker == null) throw new IllegalArgumentException("unknown channel: " + name);
return maker.get();
}
}
// Callers depend on the factory and the interface, nothing else.
class OrderService {
private final NotifierFactory notifiers;
OrderService(NotifierFactory notifiers) { this.notifiers = notifiers; }
void confirm(String channel, String user) { notifiers.create(channel).send(user, "order confirmed"); }
}
class Main {
public static void main(String[] args) {
// Composition root: the only code that names concrete classes and their settings.
NotifierFactory factory = new NotifierFactory();
factory.register("email", () -> new EmailNotifier("smtp.internal"));
factory.register("sms", () -> new SmsNotifier("sms-key"));
// A new channel is one line here. OrderService doesn't change.
factory.register("push", () -> (to, text) -> System.out.println("push to " + to + ": " + text));
OrderService orders = new OrderService(factory);
orders.confirm("email", "ana"); // email via smtp.internal to ana: order confirmed
orders.confirm("push", "ana"); // push to ana: order confirmed
}
}
  • Callers never say new. OrderService knows NotifierFactory and Notifier. It compiles without SmsNotifier, and a test can hand it a factory that builds fakes.
  • A registry beats a switch. A switch inside the factory moves the problem into one place. That’s already a big improvement. A map from name to builder goes further. A new channel is a register call at startup, and the factory itself is never edited. That’s the open-closed principle applied to creation.
  • Settings live with the registration. The SMTP host and API key appear once, in the composition root, not in every caller. (The composition root is the startup code that wires the app together.)
  • Fail loudly. An unknown name throws unknown channel: slack, and registering a name twice throws too. Silently returning null or a default channel hides configuration bugs until a customer finds them.
Interviewer

We’re adding Slack notifications. Walk me through what changes.

What they are testing

Whether the answer is “a SlackNotifier class and one register line at startup”, with no service edited. Adding a new else if in each service is the red flag, and so is a factory that returns null for unknown names.

Say this out loud

Services shouldn’t know which notifier classes exist or how they’re configured. I’ll put a factory in front: it maps a channel name to a builder, services ask it by name and get a Notifier back. A new channel is one registration at startup.

Medium

Payment providers by region

  • Abstract factory
  • Parts that must match
  • Choosing the family in one place

A checkout charges cards through Acme in the US. Since last quarter, it charges through Globex in the EU. Checkout needs two parts from the provider: a charger, and a refunder for cancellations. A charge made through one provider can only be refunded through the same provider. Each provider rejects ids it didn’t issue.

Checkout picks each part separately, with one branch per part:

Checkout.javacheckout.pyCheckout.cscheckout.gocheckout.ts
// Before: checkout picks the charger and the refunder separately, one switch each.
interface Charger { String charge(int cents); } // returns the provider's charge id
interface Refunder { void refund(String chargeId); }
class AcmeCharger implements Charger {
public String charge(int cents) { return "acme_ch_1"; }
}
class AcmeRefunder implements Refunder {
public void refund(String id) {
if (!id.startsWith("acme_")) throw new IllegalArgumentException("not an Acme charge: " + id);
System.out.println("Acme refunded " + id);
}
}
class GlobexCharger implements Charger {
public String charge(int cents) { return "gx-1"; }
}
class GlobexRefunder implements Refunder {
public void refund(String id) {
if (!id.startsWith("gx-")) throw new IllegalArgumentException("not a Globex charge: " + id);
System.out.println("Globex refunded " + id);
}
}
class Checkout {
private final Charger charger;
private final Refunder refunder;
Checkout(String region) {
// flaw: two switches that must always agree, and nothing makes them. Last quarter,
// EU payments moved to Globex. Someone updated the first switch and missed the second.
charger = switch (region) {
case "EU" -> new GlobexCharger();
default -> new AcmeCharger();
};
refunder = switch (region) {
case "EU" -> new AcmeRefunder(); // still the old provider
default -> new AcmeRefunder();
};
}
String pay(int cents) { return charger.charge(cents); }
void cancel(String chargeId) { refunder.refund(chargeId); }
}
class Main {
public static void main(String[] args) {
Checkout eu = new Checkout("EU");
String id = eu.pay(1999); // charged through Globex
try {
eu.cancel(id); // refunded through Acme
} catch (IllegalArgumentException e) {
// The customer was charged and can't get their money back. Found in production.
System.out.println("refund failed: " + e.getMessage()); // refund failed: not an Acme charge: gx-1
}
}
}

When EU moved to Globex, someone updated the charger’s branch and missed the refunder’s. Every EU charge now goes through Globex and every EU refund through Acme, which rejects it. The code compiles, each part works on its own, and the bug appears only when an EU customer cancels.

Make the family the unit of choice. A PaymentProvider creates every part checkout needs, and each provider class creates parts that belong together. Checkout receives one provider and takes all its parts from it:

Medium: one provider builds a matching charger and refunder

built from one

creates

creates

creates

creates

Checkout

+pay(cents) : chargeId

+cancel(chargeId)

«interface»

PaymentProvider

+charger() : Charger

+refunder() : Refunder

AcmeProvider

GlobexProvider

«interface»

Charger

+charge(cents) : chargeId

«interface»

Refunder

+refund(chargeId)

AcmeCharger

AcmeRefunder

GlobexCharger

GlobexRefunder

built from one

creates

creates

creates

creates

Checkout

+pay(cents) : chargeId

+cancel(chargeId)

«interface»

PaymentProvider

+charger() : Charger

+refunder() : Refunder

AcmeProvider

GlobexProvider

«interface»

Charger

+charge(cents) : chargeId

«interface»

Refunder

+refund(chargeId)

AcmeCharger

AcmeRefunder

GlobexCharger

GlobexRefunder

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation

Switch between the two designs and run an EU refund:

Checkout
charger
refunder

Main.javacheckout.pyProgram.csr.gocheckout.ts
// Abstract factory: build a family of objects that must match. A charge made through
// one payment provider can only be refunded through the same provider, so the
// charger and the refunder must always come from the same family.
interface Charger { String charge(int cents); } // returns the provider's charge id
interface Refunder { void refund(String chargeId); }
// The abstract factory: one method per product in the family.
interface PaymentProvider {
Charger charger();
Refunder refunder();
}
// The Acme family.
class AcmeCharger implements Charger {
private int next = 1;
public String charge(int cents) { return "acme_ch_" + next++; }
}
class AcmeRefunder implements Refunder {
public void refund(String chargeId) {
// Each provider only understands its own ids. Mixing families fails here, at run time,
// in production. The factory exists so this branch never runs.
if (!chargeId.startsWith("acme_")) throw new IllegalArgumentException("not an Acme charge: " + chargeId);
System.out.println("Acme refunded " + chargeId);
}
}
class AcmeProvider implements PaymentProvider {
public Charger charger() { return new AcmeCharger(); }
public Refunder refunder() { return new AcmeRefunder(); }
}
// The Globex family.
class GlobexCharger implements Charger {
private int next = 1;
public String charge(int cents) { return "gx-" + next++; }
}
class GlobexRefunder implements Refunder {
public void refund(String chargeId) {
if (!chargeId.startsWith("gx-")) throw new IllegalArgumentException("not a Globex charge: " + chargeId);
System.out.println("Globex refunded " + chargeId);
}
}
class GlobexProvider implements PaymentProvider {
public Charger charger() { return new GlobexCharger(); }
public Refunder refunder() { return new GlobexRefunder(); }
}
// Checkout receives one provider and takes both products from it, so it can't pair an
// Acme charger with a Globex refunder. It never names a concrete class.
class Checkout {
private final Charger charger;
private final Refunder refunder;
Checkout(PaymentProvider provider) {
this.charger = provider.charger();
this.refunder = provider.refunder();
}
String pay(int cents) { return charger.charge(cents); }
void cancel(String chargeId) { refunder.refund(chargeId); }
}
class Main {
// The one place that maps a region to a provider. Moving EU to Globex is one line here,
// and it moves the charger and the refunder together.
static PaymentProvider forRegion(String region) {
return switch (region) {
case "EU" -> new GlobexProvider();
default -> new AcmeProvider();
};
}
public static void main(String[] args) {
Checkout eu = new Checkout(forRegion("EU"));
eu.cancel(eu.pay(1999)); // Globex refunded gx-1
Checkout us = new Checkout(forRegion("US"));
us.cancel(us.pay(1999)); // Acme refunded acme_ch_1
}
}
  • The family is chosen once. forRegion is the only code that maps a region to a provider. Moving EU to Globex is one line, and it moves every part together, so a mismatched pair can’t be built.
  • One method per part. The abstract factory has charger() and refunder(). Say a provider gains a third part, like a dispute handler. The interface gains a method, and the compiler lists every provider that must implement it. That’s the safety, and it’s also the cost. Adding a part touches every family. Adding a family touches nothing else.
  • Checkout never names a provider. It takes a PaymentProvider and asks for parts. A test passes a fake provider whose parts record calls, without touching a payment API.
  • It’s two levels of factory. forRegion is a simple factory that picks a family. Each provider is an abstract factory that builds the parts. Real code often stacks them like this.
Interviewer

We support two payment providers per region now, and refunds are failing in Europe. How would you make this class of bug impossible?

What they are testing

Whether you see that the charger and refunder are a family that must vary together. And whether you reach for one object that builds both, chosen in one place. The red flag is adding a check that the ids match at refund time. That finds the bug in production, instead of making it impossible to build.

Hard

Database clients by URL

  • Choosing a driver by URL scheme
  • Caching what it creates
  • Thread-safe get-or-create
  • Owning cleanup

A service talks to Postgres for orders and Redis for sessions, and its configuration names each store by URL: postgres://db/orders, redis://cache:6379. Opening a client is expensive: it dials the server and fills a connection pool. The database allows a fixed number of connections in total, so each URL must get one shared client, opened once and closed on shutdown. In tests, the same URLs should open in-memory fakes.

This factory doesn’t just choose a class. It owns what it creates: it caches one client per URL, makes sure two threads can’t both open one, and closes everything at the end. That last part is the one simple factories never need.

The obvious cache is “look it up. If it’s missing, create it and store it”. On a cold start, many requests arrive at once. Every request that arrives while the first pool is still opening also finds the cache empty:

  1. req 1·
  2. req 2·
  3. req 3·
  4. req 4·
  5. req 5·
  6. req 6·
  7. req 7·
  8. req 8·

Connection pools to postgres://db/orders

Start the requests. Opening a pool takes a while, and requests keep arriving meanwhile.

Hard: drivers by scheme, one cached client per URL

picks by scheme

*

caches and closes

*

opens

StoreFactory

-drivers: Map<scheme, StoreDriver>

-clients: Map<url, Store>

+register(scheme, StoreDriver)

+get(url) : Store

+close()

«interface»

StoreDriver

+open(url) : Store

«interface»

Store

+close()

PooledStore

MemoryStore

picks by scheme

*

caches and closes

*

opens

StoreFactory

-drivers: Map<scheme, StoreDriver>

-clients: Map<url, Store>

+register(scheme, StoreDriver)

+get(url) : Store

+close()

«interface»

StoreDriver

+open(url) : Store

«interface»

Store

+close()

PooledStore

MemoryStore

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation
Two requests ask for the same URL at once
postgres driverStoreFactoryRequest BRequest Apostgres driverStoreFactoryRequest BRequest Akey already claimed: B waitslater, close(): newest firstget("postgres://db/orders")missing: claim the keyopen(url)get("postgres://db/orders")PooledStorestoresame storepostgres driverStoreFactoryRequest BRequest Apostgres driverStoreFactoryRequest BRequest Akey already claimed: B waitslater, close(): newest firstget("postgres://db/orders")missing: claim the keyopen(url)get("postgres://db/orders")PooledStorestoresame store
Scroll or pinch to zoom, drag to pan
  • Color key
  • Database
  • Facade
StoreFactory.javastore_factory.pyStoreFactory.csstore_factory.gostore-factory.ts
import java.net.URI;
import java.util.*;
import java.util.concurrent.*;
// A store client: one open connection pool to a database or cache.
interface Store extends AutoCloseable {
String describe();
@Override
void close(); // no checked exception, so shutdown code stays simple
}
// One driver per URL scheme. Opening is expensive: it dials the server and fills a pool.
interface StoreDriver {
Store open(URI url);
}
final class PooledStore implements Store {
private final URI url;
PooledStore(URI url) {
this.url = url;
System.out.println("connecting to " + url);
}
public String describe() { return "pool for " + url; }
public void close() { System.out.println("closing " + url); }
}
// Fake used by tests: same interface, no network.
final class MemoryStore implements Store {
private final URI url;
MemoryStore(URI url) { this.url = url; }
public String describe() { return "memory store for " + url; }
public void close() {}
}
// The factory chooses a driver by URL scheme and owns what it creates: one shared
// client per URL, opened once, closed at shutdown. Callers only ever say get(url).
final class StoreFactory implements AutoCloseable {
private final Map<String, StoreDriver> drivers = new ConcurrentHashMap<>();
// computeIfAbsent runs the open function at most once per key, atomically, so eight
// threads asking for the same URL on a cold start get one pool, not eight.
// Tradeoff: it holds a lock on that map bin while a slow server is dialled; fine for a
// handful of URLs opened at startup, not for thousands of keys.
private final ConcurrentHashMap<String, Store> clients = new ConcurrentHashMap<>();
// Remembered in open order so shutdown closes them in reverse, like nested try blocks.
private final Deque<Store> opened = new ConcurrentLinkedDeque<>();
private volatile boolean closed;
void register(String scheme, StoreDriver driver) {
// Duplicates fail loudly: two modules silently fighting over "postgres" is a bug.
if (drivers.putIfAbsent(scheme, driver) != null) {
throw new IllegalStateException("driver already registered: " + scheme);
}
}
Store get(String url) {
if (closed) throw new IllegalStateException("factory is closed");
return clients.computeIfAbsent(url, u -> {
URI uri = URI.create(u);
StoreDriver driver = drivers.get(uri.getScheme());
// Fail with what the caller needs to fix it: the bad scheme and the valid ones.
// A thrown exception leaves no entry behind, so a later call can try again.
if (driver == null) {
throw new IllegalArgumentException("no driver for '" + uri.getScheme()
+ "' (registered: " + new TreeSet<>(drivers.keySet()) + ")");
}
Store store = driver.open(uri);
opened.push(store);
return store;
});
}
@Override
public void close() {
closed = true;
// One store failing to close must not leave the others open.
for (Store store : opened) {
try {
store.close();
} catch (RuntimeException e) {
System.out.println("close failed: " + e.getMessage());
}
}
opened.clear();
clients.clear();
}
}
class Main {
public static void main(String[] args) throws Exception {
StoreFactory stores = new StoreFactory();
stores.register("postgres", PooledStore::new);
stores.register("redis", PooledStore::new);
// Eight request threads ask for the same database at once, as on a cold start.
ExecutorService pool = Executors.newFixedThreadPool(8);
List<Future<Store>> results = new ArrayList<>();
for (int i = 0; i < 8; i++) results.add(pool.submit(() -> stores.get("postgres://db/orders")));
Set<Store> distinct = Collections.newSetFromMap(new IdentityHashMap<>());
for (Future<Store> r : results) distinct.add(r.get());
pool.shutdown();
System.out.println("clients for 8 requests: " + distinct.size()); // 1
stores.get("redis://cache:6379");
try {
stores.get("mongo://reports/db");
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // no driver for 'mongo' (registered: [postgres, redis])
}
stores.close(); // closing redis..., then closing postgres...
// In a test, a fresh factory maps the same scheme to a fake. The code under test
// still asks for "postgres://db/orders" and never knows.
StoreFactory test = new StoreFactory();
test.register("postgres", MemoryStore::new);
System.out.println(test.get("postgres://db/orders").describe());
}
}
  • Get-or-create must be one atomic step. Java’s computeIfAbsent runs the creator at most once per key. C#’s GetOrAdd does not promise that, so the value is a Lazy<Store>. Two racing threads may create two cheap Lazy wrappers, but only the stored one ever opens. Python and Go hold a lock around the check and the create. TypeScript has one thread, but get awaits the handshake. So it caches the promise right away. If it cached only finished stores, every request that arrives during the handshake would open its own.
  • Failures must not be cached. If opening fails, the next call should try again. It shouldn’t get the same old error forever. Java’s computeIfAbsent and the Python and Go versions store nothing on failure. C#’s Lazy and the TypeScript promise do cache failures. So the code removes the entry when that happens.
  • Errors say how to fix them. no driver for 'mongo' (registered: postgres, redis) names the bad input and the valid options. The person reading it at 3 a.m. doesn’t need to open the source.
  • Whoever creates, closes. Callers never close a store they got from get: it’s shared, and closing it would break every other user. The factory closes all of them on shutdown, newest first, and keeps going if one fails, so one bad close doesn’t leak the rest.
  • Tests swap the driver, not the code. A test registers MemoryStore for the postgres scheme on a fresh factory. The code under test still asks for postgres://db/orders.
  • The lock has a cost. Holding a single lock while a slow server is dialled also blocks gets for other URLs. With a few URLs opened at startup, that’s fine. With many, use a lock or sync.Once per key. That’s what computeIfAbsent and Lazy already give you.
Interviewer

Our service opens a new database pool on every request and we keep hitting the connection limit. Design the fix.

What they are testing

Whether you cache one client per database behind a factory, make get-or-create atomic, and give the factory ownership of closing. A plain “if missing, create” check (which races on cold start) is the red flag, and so is letting callers close a client they share.

Say this out loud

The factory picks a driver by URL scheme, and keeps one client per URL. Get-or-create is atomic, so a cold start opens each pool once. Callers never close what they get, because it’s shared. The factory closes everything on shutdown, newest first.

Use a factory in four cases. The concrete class depends on data (a setting, a request, a URL). Creation needs settings that callers shouldn’t know. Parts must match. Or created objects need caching or cleanup. Keep calling the constructor for value objects, and for classes with one implementation that nobody needs to swap.

Language Usual form Built in
Java Static factory methods (of, from, getInstance), a Map<String, Supplier<T>> registry List.of, Calendar.getInstance, ServiceLoader
Python A plain function, a dict of classes or callables, @classmethod alternative constructors datetime.fromisoformat, logging.getLogger
C# Static Create methods, a Dictionary<string, Func<T>>, Lazy<T> for once-only HttpClientFactory, DbProviderFactories, ILoggerFactory
Go New... functions (Go has no constructors), a map of funcs registered in init() database/sql drivers registered by name, image.RegisterFormat
TypeScript A function returning an interface, a Map of builders, static create document.createElement, Array.from

Go’s database/sql is the Hard level in the standard library. Drivers register themselves by name in init(). sql.Open("postgres", dsn) picks one. And the returned *sql.DB is a pool, meant to be shared and closed once.

Pattern Looks like Factory because The difference
Builder Both create objects A factory picks which object to create in one call. A builder assembles one complex object step by step.
Strategy A registry of strategies looks like a registry of products Strategy is about swapping behavior. A factory is about creating objects; it often creates the strategy.
Singleton getInstance() is a factory method A singleton limits a class to one instance. The Hard level’s factory caches one instance per key and is itself an ordinary object.
Dependency injection Both keep new out of business code A DI container is a general factory configured for the whole app. A hand-written factory is the same idea for one family of objects.
Service locator Both hand out objects by key Callers of a locator reach into a global to pull dependencies, which hides them. A factory is passed in, so the dependency is visible.

On this site:

  • Rate limiter: a BucketFactory creates the right kind of bucket for each new key, per rule.
  • Parking lot: a factory method builds a floor with consistent ids and distances.
  • Strategy: a factory reads each merchant’s settings and builds the shipping rule tree.

In real systems, you’ll find factories in:

  • JDBC and database/sql drivers.
  • HttpClientFactory in .NET.
  • Cloud SDKs that pick a credentials provider from the environment.
  • UI toolkits that create native widgets per platform (the textbook abstract factory).
  • Every dependency injection container.
Interviewer asksWhat's the difference between factory method and abstract factory?

Factory method is one overridable method. A subclass decides which single object the base class creates. Abstract factory is an object with several creation methods, one per part of a family. So parts that must match are built together. Factory method uses inheritance. Abstract factory uses composition, and is passed in like any other dependency.

Interviewer asksWhy not just use a switch statement?

A switch inside one factory is fine, and often the right first step. Creation is already in one place. A registry goes further when new types are added often, or by other modules, because adding one doesn’t edit the factory. The switch has one advantage. The compiler can check that it covers every enum value. A string registry can’t.

Interviewer asksHow do factories help testing?

Callers depend on the factory and the product interface. So a test passes a factory that returns fakes, or registers a fake driver for the same name. Nothing in the code under test changes. And no real database, payment API or SMTP server is touched.

Interviewer asksWho should close an object a factory created?

Whoever owns it. If the factory hands out a new object per call, the caller owns it, and closes it. If the factory caches and shares it, the factory owns it. Then callers must not close it, and the factory closes everything on shutdown. Mixing the two is how shared clients get closed under other users.

Decision We chose We gave up
Where creation lives One factory; callers never say new One more indirection to read through
How to choose A registry of builders by name Compile-time checking of names
Parts that must match An abstract factory per family, chosen once Adding a part touches every family
Expensive products One cached client per key The factory now has state and a lifecycle
Concurrent first use Atomic get-or-create (computeIfAbsent, Lazy, lock, cached promise) A slow open can block other gets if the lock is shared
Failures Never cached; errors list the valid options Nothing; it’s the fix
Cleanup The factory closes what it shares, newest first Callers can’t close shared clients early
  • Strategy, which a factory often creates
  • Builder, for building one complex object step by step
  • Singleton, and why a cached factory is usually better
  • Joshua Bloch, Effective Java, Item 1: “Consider static factory methods instead of constructors”