Skip to content
beginner Phase 3 · Design Patterns at Scale

Creational Patterns and Factories

Centralize object creation behind factories and controlled lifecycles.

50m
0 problems
Topic Progress 0%

Hiding new: The Four Things Construction Sites Get Wrong

Look at every new keyword in a service and ask what the caller now owns: choosing the concrete class, ordering the constructor arguments correctly, managing the object's lifetime, and deciding what happens when construction fails. In application code, four of those five concerns belong to the infrastructure, not to the caller — and factories hide concrete construction so callers depend on abstractions only.

Here is a construction site accumulating all four mistakes:

// A caller doing its own construction owns every failure mode:
PaymentGateway gw = new StripeGateway(
    System.getenv("STRIPE_KEY"),   // null at 3am in prod? caller's problem
    new OkHttpClient(Duration.ofSeconds(30)),  // who closes this?
    Region.EU);                    // caller hardcodes a deployment detail

// Selection logic grows next to it:
if (config.isTest()) gw = new FakeGateway();
else if (config.isLegacy()) gw = new AdyenGateway(config.key());

The factory absorbs each concern in one place:

public interface PaymentGateway {
    ChargeResult charge(Money m, CardToken t);
}

public final class GatewayFactory {
    private final Config config;
    private final OkHttpClient shared;   // one client, one lifecycle

    public PaymentGateway create() {
        return switch (config.gateway()) {
            case STRIPE  -> new StripeGateway(config.require("stripe.key"), shared, config.region());
            case ADYEN   -> new AdyenGateway(config.require("adyen.key"), shared);
            case FAKE    -> new FakeGateway();
        };
    }
}

The caller now asks for a PaymentGateway and holds no knowledge of vendor classes, environment variable names, or HTTP client lifecycles. The four absorbed concerns are worth naming precisely, because they are also the four questions to ask when deciding whether a construction site deserves a factory:

Concern Without a factory With a factory
Selection if/else over concrete classes, scattered one switch, owned and tested once
Validation nulls discovered at first call missing config fails at the composition root
Lifetime per-call clients leak sockets shared resource created once
Substitution tests edit production call sites tests inject a fake at wiring

The pattern's interview name is the simple factory (a static or injected function returning an abstraction), distinct from the factory method — a subclass-overridable creation method embedded in a creator class, the GoF form used when the framework decides the product but the subclass decides the concrete variant. A template-rendering engine where createParser() returns JSON or XML parsers depending on the document type is the canonical factory-method shape. The honest guidance for application code: most teams need the simple factory or none at all; the factory method earns its name inside frameworks and class hierarchies where the inversion is real.

One warning against factory fever: the pattern pays when construction has decisions in it (selection, validation, shared lifetime, fail-fast behavior). A class with a fixed constructor and no variants gains nothing from a factory layer — new Order(clock, idGenerator) inside the same package is perfectly direct, and wrapping it in OrderFactory.create(clock, idGenerator) is indirection with no decision to hide. The next chapter's pattern, where whole families of objects must stay consistent, is where factories genuinely become architectural — Abstract Factory families.

Abstract Factory: Keeping a Family of Related Objects Consistent

The simple factory returns one object. A different problem appears when a subsystem needs a set of related objects that must all agree — and mixing members of different sets produces bugs that compile cleanly. The classic setting: the same application code runs against different storage backends in different environments, and each backend is a family of collaborating objects, not a single class.

// The family interface — one method per product in the set:
public interface StorageFactory {
    BlobStore blobs();
    QueueStore queues();
    LockService locks();
}

public final class LocalstackFactory implements StorageFactory {
    public BlobStore blobs()   { return new S3BlobStore(localS3(), "local"); }
    public QueueStore queues() { return new SqsQueueStore(localSqs()); }
    public LockService locks() { return new DdbLockStore(localDdb()); }
}

public final class RealAwsFactory implements StorageFactory {
    public BlobStore blobs()   { return new S3BlobStore(awsS3(), bucket); }
    public QueueStore queues() { return new SqsQueueStore(awsSqs()); }
    public LockService locks() { return new DdbLockStore(awsDdb()); }
}

Now count what the abstract factory prevents: an integration test that reads blobs from Localstack but takes locks against the real DynamoDB — the mismatch that costs an afternoon of confused debugging when someone's dev environment half-connects to production. Because the whole family arrives from one object selected once at the composition root, consistency is structural rather than remembered. The pattern's guarantee in one sentence: code using the factory never names a concrete member of any set, so no call site can mix sets.

The GoF vocabulary for the shapes you will meet: abstract factory — the family interface above, chosen by environment or tenant; factory method — one product, decided by subclass; prototype — construction by cloning an existing configured instance, common in rule engines that stamp out copies of a template with per-tenant overrides. They answer different questions (which set, which variant, which copy), and naming the right one in an interview is half the answer.

Two costs, honestly, because families ossify. Adding a product type to the family (say, a SearchIndex store) forces edits to every implementing factory — the same expression-problem pressure switch statements face; if product types churn, consider making the product itself an open registry instead of a closed interface method. And the family interface can bloat toward the fat-interface trap: if the local implementation throws UnsupportedOperationException for three of six methods, the family is claiming more than it delivers, and segregation says split it. Families that are genuinely stable (storage, messaging, observability backends — things chosen once per environment) are exactly where abstract factories pay rent.

The bridge to the modern idiom is direct: dependency injection containers automate constructor wiring across large services, and a DI container is an abstract factory generalised — instead of one family interface with hand-written methods, you register each product binding and the container assembles whole object graphs per scope. That generalization is the next section's subject, including when the container itself becomes the anti-pattern. Testing benefits the same way the composition root does: with the family arriving from one seam, an integration test constructs a LocalstackFactory against the testcontainers stack and exercises every storage collaborator through the same interfaces production uses, while pure unit tests substitute an in-memory factory without any containers at all. The alternative — reaching for concrete S3 and SQS classes per test — drags credential handling and network timing into suites that only wanted to assert business behavior, and that friction is how test coverage quietly evaporates.

When the Container Takes Over: Lifetimes, Scopes, and the Locator Trap

A service with twelve collaborators per handler and forty handlers does not want forty hand-written factory methods; it wants declarations. That is the territory of dependency injection containers — Spring, Guice, Dagger, Autofac, the .NET container — and each is an abstract factory generalized: bindings map abstractions to implementations, and the container constructs whole object graphs on demand.

// Declarations at the composition root, one place, reviewed like code:
bind(PaymentGateway.class).to(StripeGateway.class).in(Singleton.class);
bind(HttpClient.class).toInstance(sharedClient);
bind(OrderRepository.class).to(SqlOrderRepository.class).in(Scoped.class);
bind(RefundOrder.class);                     // constructor auto-wired

// Any handler asks for the use case; the graph is built once per scope:
public RefundHandler(RefundOrder refundOrder) { ... }

The feature that makes containers genuinely architectural — and the one most often misused — is lifetime management. A singleton binding is created once and shared for the process lifetime, so it must hold no mutable per-request state; a scoped binding is created once per request scope (or per message, in a consumer) and disposed with it; a transient binding is constructed per request, which is correct for stateless helpers and a silent performance trap for heavyweight objects like HTTP clients or parsers. The classic production bug follows directly: an ORM session registered as a singleton accumulates every request's entities until memory balloons, or a stateless-looking cache accumulates cross-request data and leaks tenant boundaries. Writing the intended lifetime beside each binding, with its thread-safety claim, is the cheapest incident prevention available:

PaymentGateway  singleton    thread-safe: no mutable fields
HttpClient      singleton    connection pool, closed at shutdown
OrderRepository scoped       wraps the per-request session
Clock           singleton    stateless

And the line that keeps the container from eating the architecture: resolve at construction, never query at call time. The moment business code calls locator.get(Foo.class) inside a method, three regressions arrive together — the dependency set becomes invisible again, missing bindings fail at first production call instead of at startup, and the module cannot be constructed outside the container. Containers earn their complexity by resolving the entire graph in the composition root and handing plain constructors their dependencies; used that way, your classes are container-agnostic and the previous phases' constructor-injection discipline survives intact.

The decision table for adoption, because hand-written wiring is a legitimate rival: under roughly ten bindings per entry point, hand-written wiring is shorter to read and to debug; a container earns its place when the binding count reaches dozens, when scoped lifetimes are pervasive, or when conditional registration (stub in dev, real in prod, per-tenant families) would otherwise become hand-rolled branching. If you adopt one, treat its configuration as the system's most important file — it is the abstract factory of record — and give it the same review, testing, and documentation standards as the domain code it wires together. A concrete incident pattern illustrates why the locator rule and lifetime documentation matter together. A team registers a metrics registry as a singleton that internally holds a per-request label map; under load, label cardinality explodes and the monitoring backend itself degrades — the application took down its own observability. The postmortem fix was not a bigger backend: the registry was split into a truly stateless singleton plus a scoped per-request label collector, and the binding comment that names this split now prevents its return. Lifetime mistakes are rarely theoretical; they surface as the exact classes of production failure that are hardest to trace back to a configuration line.

Quiz

1. Which four construction concerns does a factory absorb for its callers?

Question 1 options

2. An integration test environment accidentally reads blobs from Localstack but takes distributed locks against real DynamoDB. Which pattern directly prevents this class of bug?

Question 2 options

3. A DI container registers the ORM session as a singleton. What failure should you predict, and what is the correct binding?

Question 3 options

4. Business code calls `locator.get(OrderRepository.class)` inside method bodies. What is the architectural regression?

Question 4 options

Flashcards

Question

What four concerns does a construction site own without a factory, and where should each live instead?

Answer

Selection (which concrete class — one tested switch in the factory), validation (missing config fails at the composition root, not as a null at 3 a.m.), lifetime (shared HTTP client created once, closed at shutdown), and substitution (tests inject fakes at wiring instead of editing production call sites). The factory concentrates all four so callers hold only the abstraction.

Question

Distinguish simple factory, factory method, and abstract factory by the question each answers.

Answer

Simple factory: which variant of one product? — a function returning an abstraction, the workhorse for application code. Factory method: which variant does the subclass choose? — a creation method overridden in a hierarchy, the GoF form used inside frameworks. Abstract factory: which consistent family of related products? — one interface with a method per product, selected once per environment or tenant.

Question

What does the abstract factory guarantee, and what two costs come with it?

Answer

Guarantee: code never names a concrete member of any set, so members of different families cannot mix (no Localstack blobs with real DynamoDB locks). Costs: adding a product type edits every factory implementation (closed-interface pressure — use an open registry if product types churn), and the family interface can bloat until implementations throw UnsupportedOperationException — split it per interface segregation.

Question

Compare singleton, scoped, and transient DI lifetimes with the failure each causes when chosen wrong.

Answer

Singleton (one per process): wrong for anything holding per-request state — an ORM session singleton balloons memory and leaks tenant data. Scoped (one per request/message, disposed with the scope): correct for sessions and repositories. Transient (per resolution): correct for stateless helpers, a silent performance trap for heavyweight objects like HTTP clients. Write the lifetime and thread-safety claim beside every binding.

Question

When does a DI container earn its complexity over hand-written composition-root wiring, and what is the locator rule?

Answer

Earns it at dozens of bindings per entry point, pervasive scoped lifetimes, or conditional registration (stub in dev, real in prod, per-tenant families) that would otherwise become hand-rolled branching. Below roughly ten bindings, hand-written wiring is shorter to read and debug. Rule: resolve the whole graph at construction and inject via constructors — never query the container from business logic (service-locator trap).

Revision Notes

Key Takeaways

  • 1. Factories pay when construction contains decisions — selection, validation, shared lifetime, fail-fast — and add nothing when the constructor is fixed and local
  • 2. A factory concentrates the if/else over concrete classes into one tested switch, which is also what lets tests substitute fakes at wiring instead of editing production call sites
  • 3. Abstract factory's guarantee is structural: whole families arrive from one selection point, so mixed-set bugs (Localstack blobs with production locks) cannot compile into existence
  • 4. Family interfaces ossify in two directions — product-type additions touch every factory, and bloat produces throwing implementations — so keep families stable or split them
  • 5. DI containers are abstract factories with lifetime management: write each binding's scope and thread-safety claim beside it, and resolve at construction, never via locators in logic

Interview Tips

  • • 'Why hide new behind a factory?' — walk the four absorbed concerns (selection, validation, lifetime, substitution) and give the fail-fast example: missing config dies at the composition root instead of a null at 3 a.m.
  • • 'Simple factory vs factory method vs abstract factory?' — answer by question: which variant, which subclass-decided variant, which family; adding the prototype (cloned templates) shows the full vocabulary
  • • 'When would you NOT use a factory?' — fixed constructor, no variants, same package: the indirection hides no decision; being able to decline patterns is the architect's tell
  • • 'How do you prevent wrong DI lifetimes?' — declare scope plus thread-safety claim per binding; cite the singleton ORM-session failure (memory balloon, cross-request leak) and the transient-HTTP-client trap

Cheat Sheet

Creational Patterns and Factories

Why hide new: the caller of raw construction owns selection, validation, lifetime, and failure — all four belong at the composition root.

Concern Without factory With factory
Selection scattered if/else one tested switch
Validation null fails at first call missing config fails at wiring
Lifetime per-call HTTP clients one shared client, closed at shutdown
Substitution tests edit production sites fakes injected at wiring

Pattern chooser:

  • Which variant of one product? → simple factory (application default)
  • Which variant does a subclass pick? → factory method (framework hierarchies)
  • Which consistent family? → abstract factory (env/tenant backends: blobs+queues+locks from one object)
  • Which copy of a template? → prototype (cloned rule templates per tenant)

Abstract factory trade: families prevent cross-set mixing; adding a product type edits every factory — open registry if product types churn; split bloat per ISP.

DI container = generalized family factory:

PaymentGateway  singleton    thread-safe: no mutable fields
HttpClient      singleton    pool, closed at shutdown
OrderRepository scoped       wraps per-request session

Adopt at dozens of bindings, pervasive scopes, or conditional registration. Resolve at composition root, inject via constructors, never query a locator from logic.

Continue Learning