← blog

The Trouble with new — Factories, Builder, and Singleton

The creational patterns are really one question: who is allowed to run new, and how much do they have to know to do it? Factories, Builder, and the honest story of Singleton in the dependency-injection era.

Every pattern in this post is an answer to one deceptively small question: who runs new, and how much do they have to know to do it? Of all the pattern buckets this is the one that overlaps most with what people call “clean code” — because creating objects is where dependencies are born.

To feel why that question matters, watch one innocent line age badly. Deep inside OrderService:

this.mailer = new SmtpMailer("smtp.gmail.com", 587, user, password);

It works for months. Then two things happen. Someone writes the first unit test — all it wants to prove is “a paid order sends a confirmation email” — and finds it can’t be done: constructing an OrderService opens a real SMTP connection, and there is no way to slip a fake mailer in. Then the company moves to SendGrid, and the migration turns out to live inside order code — and inside invoice code and signup code too, because each of them ran its own new SmtpMailer. One keyword quietly baked three decisions into classes that only ever wanted to send mail: which concrete class, what its constructor needs, and where the credentials come from.

Dependencies are born at new — and the creational patterns exist to make sure they’re born somewhere deliberate, instead of everywhere.

The reassuring part: you already use all four of these daily. The four intents, one line apiece:

  • Factory — callers ask for what they need; one place decides which concrete class comes back.
  • Abstract Factory — objects that must match as a set get built by one family-picking factory.
  • Builder — a complex object gets assembled step by readable step, and can’t exist half-made.
  • Singleton — “exactly one instance” is a real need; the classic getInstance() enforces it inside the class. Dependency injection — its own section, right after Singleton — enforces the same oneness in the wiring.

01 · Factory — the caller asks, one place decides#

You tap “book a cab.” A car shows up. You never chose which car — not the plate number, not the model, not the driver. You asked for a ride (the interface) and the dispatch system (one deciding place) picked the concrete car. Nobody would want the alternative: every passenger personally maintaining knowledge of the whole fleet.

Our order system has the same cast, wearing shipping uniforms. A parcel is a passenger that needs a ride to the customer: the ride is the Carrier interface, and the fleet has three concrete vehicles — FreightCarrier (the truck, for anything heavy), CourierCarrier (the bike, for express), PostalCarrier (the cheap seat everyone else gets). The only open question is who plays dispatch.

Here’s the version nobody would want as a passenger — dispatch-by-passenger, where every service keeps its own fleet knowledge and builds its own vehicle:

public class OrderService {
    public void dispatch(Order order) {
        Carrier carrier;
        if (order.weightGrams() > 20_000) {
            carrier = new FreightCarrier(config.freightApiKey(), config.depotCode());
        } else if (order.express()) {
            carrier = new CourierCarrier(config.courierApiKey());
        } else {
            carrier = new PostalCarrier();
        }
        carrier.ship(order);
    }
}

Then the returns team needs to ship things back, and copy-pastes the ladder into ReturnsService. Two diseases now spread together:

  • The choice logic is duplicated. The business rule “over 20kg goes freight” now physically lives in two files. The day the cutoff moves to 25kg, someone updates OrderService and nobody remembers ReturnsService — so a 22kg parcel ships out by post but comes back on a freight truck. Same rule, two copies, quietly disagreeing, no error anywhere. (Strategy fought this exact disease in its duplicated if/else ladders.)
  • Construction details leak everywhere. Every copy of the ladder knows about API keys and depot codes. Add a constructor parameter to FreightCarrier and you’re editing every file that ever built one.

Boil both diseases down: knowing which class to build and how to build it is scattered across callers who only ever wanted the finished thing.

The factory contract: callers say what they need; one factory owns every new and decides which concrete class comes back — so construction knowledge lives in exactly one place.

Dispatch gets one owner#

public final class CarrierFactory {
    private final FreightConfig freight;
    private final CourierConfig courier;

    public CarrierFactory(FreightConfig freight, CourierConfig courier) {
        this.freight = freight;
        this.courier = courier;
    }

    public Carrier forShipment(Order order) {
        if (order.weightGrams() > 20_000) {
            return new FreightCarrier(freight.apiKey(), freight.depotCode());
        }
        if (order.express()) {
            return new CourierCarrier(courier.apiKey());
        }
        return new PostalCarrier();
    }
}

And both services shrink to:

public void dispatch(Order order) {
    carrierFactory.forShipment(order).ship(order);
}

Notice what did not happen: the if/else ladder didn’t disappear — it moved. That’s the honest heart of this pattern. Somewhere, some code must decide which class gets built; the win is that it’s now one place whose entire job is deciding, instead of a ladder copy-pasted into every caller. (Same lesson as Strategy’s lookup table: ladders aren’t the disease — scattered duplicate ladders are.) The other win: only the factory knows about API keys and depot codes now. FreightCarrier‘s constructor can change tomorrow and exactly one file cares.

before: everyone builds everythingOrderServiceReturnsServiceSubscriptionsFreightCourierPostalnine paths that all know constructors and keysafter: one place decidesOrderServiceReturnsServiceSubscriptionsCarrierFactoryFreightCourierPostalconstructors and keys live in one file
Who has to know how carriers are built. Left: every service knows every concrete carrier, every constructor, every config value — a change touches them all. Right: services know one factory; construction knowledge lives in one place.

One naming note so books don’t confuse you: the GoF’s official Factory Method is a narrower thing — a superclass runs the overall flow and lets subclasses override one createX() method (frameworks use it on you: JUnit creating your test instances, for example). What everyday code calls “a factory” — the class above, or a static method like the ones below — is looser and far more common. The intent is identical: one deciding place owns construction.

The JDK is full of them#

  • Java’s static factories: List.of(...), Optional.empty(), LocalDate.now(), LoggerFactory.getLogger(...). A constructor can’t have a name, can’t return a subtype, and can’t hand you a cached instance — a static factory can do all three, which is why modern Java APIs almost never expose bare constructors.
  • Node.js: express() is a factory function — you never write new Express(). Same for Buffer.from(...) and nearly every “call the module, get an object” API in the ecosystem.
  • Python: classmethod constructors like datetime.fromtimestamp(...) and dict.fromkeys(...) — named ways to build, exactly the static-factory idea.
  • Every DI framework’s @Bean / provider methods — the container is one giant “one place decides construction.”

When NOT to use it#

  • One implementation, no secrets. If there’s a single concrete class and its constructor takes what the caller already has, new is honest and clear. Factories for everything is how you end up mocked in memes about FactoryFactoryProvider.
  • The factory just forwards. create(x) { return new Thing(x); } with no decision and no hidden config adds a hop with no win — wait until there’s a real choice or real construction knowledge to centralize.

02 · Abstract Factory — pick the family once#

Buy one wireless earbud from brand A and the charging case from brand B — they simply won’t work together. Some things only make sense as a matching set.

Our shop has a set like that: everything a payment provider needs. A gateway to charge, a refund handler, and a webhook verifier that checks the provider’s signatures. Build them separately and this compiles fine:

PaymentGateway gateway   = new StripeGateway(stripeKey);
WebhookVerifier verifier = new PayPalWebhookVerifier(paypalSecret);  // wrong family — and it compiles

Stripe charges the card; PayPal’s verifier rejects every Stripe webhook; payments look successful and confirmations never send. Nothing stopped the mismatch, because each piece was created independently. Put precisely: objects that must agree with each other are constructed by code that can’t see the whole set.

What Abstract Factory guarantees: when objects only work as a matching set, one factory builds the entire set — you pick the family once, and mixing families becomes impossible.

Pick the family once#

The factory’s interface is the list of things that must match:

public interface PaymentProviderFactory {
    PaymentGateway gateway();
    RefundHandler refunds();
    WebhookVerifier webhooks();
}

public final class StripeProviderFactory implements PaymentProviderFactory {
    private final StripeConfig config;

    public StripeProviderFactory(StripeConfig config) { this.config = config; }

    @Override public PaymentGateway gateway()   { return new StripeGateway(config.apiKey()); }
    @Override public RefundHandler refunds()    { return new StripeRefunds(config.apiKey()); }
    @Override public WebhookVerifier webhooks() { return new StripeWebhookVerifier(config.signingSecret()); }
}

The family gets picked in exactly one place — startup, from config:

PaymentProviderFactory provider = switch (config.paymentProvider()) {
    case STRIPE   -> new StripeProviderFactory(stripeConfig);
    case PAYPAL   -> new PayPalProviderFactory(paypalConfig);
};

The structure, as a map — the interface is the contract “a full matching set,” and each implementing factory is one family:

classDiagram
  class PaymentProviderFactory {
    <<interface>>
    +gateway() PaymentGateway
    +refunds() RefundHandler
    +webhooks() WebhookVerifier
  }
  class StripeProviderFactory {
    -config: StripeConfig
  }
  class PayPalProviderFactory {
    -config: PayPalConfig
  }
  PaymentProviderFactory <|.. StripeProviderFactory : implements
  PaymentProviderFactory <|.. PayPalProviderFactory : implements
  StripeProviderFactory ..> StripeGateway : creates
  StripeProviderFactory ..> StripeRefunds : creates
  StripeProviderFactory ..> StripeWebhookVerifier : creates
  PayPalProviderFactory ..> PayPalGateway : creates
  PayPalProviderFactory ..> PayPalRefunds : creates
  PayPalProviderFactory ..> PayPalWebhookVerifier : creates
One interface, one factory per family. Every piece a factory creates carries its own brand — no arrow crosses families, and that's the entire guarantee.

Everything downstream asks provider for its pieces, and a Stripe gateway with a PayPal verifier is no longer a bug you can write — there’s no code path that produces it. Bonus that pays daily: tests hand the whole system a FakeProviderFactory and the entire payment family becomes fake at once, consistently.

config: STRIPEpicked once, at startupStripeProviderFactorygatewayrefundswebhooksPayPalProviderFactorygatewayrefundswebhooksStripe gateway + PayPal webhooks: no code path produces this
One factory, one matching set. The family is picked once, from config; every piece downstream comes from the same column. The cross-family combination from the broken snippet above is no longer something code can express.

Matching sets in the wild#

  • JDBC, quietly. A Connection creates Statements, which create ResultSets — every piece from the same driver family. You picked the family once, in the connection URL.
  • boto3 (Python/AWS): session = boto3.Session(profile_name="prod"), then session.client("s3"), session.client("sqs") — every client born from the session shares credentials and region. The session is an abstract factory; mixing prod-S3 with dev-SQS can’t happen by accident.
  • The GoF original: GUI toolkits with swappable look-and-feel — one factory per theme producing matching buttons, menus, scrollbars.

When NOT to use it#

  • The family has one member. A factory interface with one method is just a factory — drop the “abstract.”
  • The families never actually vary. If there will only ever be Stripe, the interface is speculation. Same rule as Strategy: wait for the second real family.
Factory vs Abstract Factory, side by side

Both hide new behind an interface, which is why they blur together from a distance. The split is what one call gets you:

FactoryAbstract Factory
The question it answers“which one?”“which family?”
One call returnsone producta matching set
The products arecompetitors — freight or courier or postalteammates — gateway and refunds and webhooks, same brand
The decision happensper order, at runtimeonce, at startup
The disease it killschoice ladders copy-pasted into callersmixed-brand sets that compile fine and break in production
CarrierFactoryforShipment(order)FreightCourierPostalforks on every order — one product backswitch (config.paymentProvider())case STRIPEcase PAYPALStripeProviderFactoryPayPalProviderFactorygateway()refunds()webhooks()StripeGatewayStripeRefundsStripeWebhooksPayPalGatewayPayPalRefundsPayPalWebhooksforks once — the whole matched column back
Both sides fork between dashed alternatives — the difference is where the fork sits and what rolls out of it. The factory forks between products on every order and hands back one. The abstract factory forks between whole families, once, and after that every piece arrives pre-matched.

They also nest: each method on PaymentProviderFactory is an ordinary factory on its own. Grouping three of them behind one interface — so no implementation can answer them with mixed brands — is exactly what the “abstract” adds.


03 · Builder — assembled step by step, and it can’t exist half-made#

Ordering at a sandwich shop is a builder. You don’t recite the full sandwich upfront in a fixed order — you answer a sequence of small questions (bread? cheese? toppings? toasted?), skip what you don’t care about, and the sandwich only starts existing when you say “that’s it.” You can’t be handed half a sandwich.

Now the constructor everyone has been bitten by:

Order order = new Order("cust-42", lines, null, "USD", true, false, null);

Quick: which boolean is gift wrap? What’s the third null? This is the telescoping constructor — as optional fields pile up, the constructor grows overloads, callers pass nulls and count commas, and a parameter-order bug (swap two booleans, everything compiles) ships to production. The underlying failure: construction with many optional parts is being forced through one rigid, unreadable call.

Builder’s deal: construction becomes a sequence of named steps — set what you need, skip what you don’t, and build() hands you a complete, checked object or refuses.

Named steps, checked finish#

public final class Order {
    private final CustomerId customerId;
    private final List<OrderLine> lines;
    private final String currency;
    private final boolean giftWrap;
    private final Address deliveryAddress;

    private Order(Builder b) {
        this.customerId = b.customerId;
        this.lines = List.copyOf(b.lines);
        this.currency = b.currency;
        this.giftWrap = b.giftWrap;
        this.deliveryAddress = b.deliveryAddress;
    }

    public static Builder builder(CustomerId customerId) {
        return new Builder(customerId);
    }

    public static final class Builder {
        private final CustomerId customerId;              // required — demanded up front
        private final List<OrderLine> lines = new ArrayList<>();
        private String currency = "USD";                  // sensible default
        private boolean giftWrap = false;
        private Address deliveryAddress;

        private Builder(CustomerId customerId) { this.customerId = customerId; }

        public Builder line(OrderLine line)        { lines.add(line); return this; }
        public Builder currency(String currency)   { this.currency = currency; return this; }
        public Builder giftWrap()                  { this.giftWrap = true; return this; }
        public Builder deliverTo(Address address)  { this.deliveryAddress = address; return this; }

        public Order build() {
            if (lines.isEmpty())        throw new IllegalStateException("an order needs at least one line");
            if (deliveryAddress == null) throw new IllegalStateException("an order needs a delivery address");
            return new Order(this);
        }
    }
}

The call site is the whole payoff — it reads like the sandwich order:

Order order = Order.builder(customerId)
    .line(new OrderLine(sku, 2))
    .deliverTo(address)
    .giftWrap()
    .build();

Plain-words walkthrough:

  • Every step has a name, so the boolean soup is gone — .giftWrap() cannot be mistaken for anything else, and steps can arrive in any order.
  • Required things are unavoidable: customerId is demanded to even start, and build() refuses to produce an order without lines or an address. The half-made object can’t escape — Order itself has no setters, arrives complete, and never changes.
  • In real Java you rarely hand-write this: Lombok’s @Builder generates the whole inner class from one annotation. Knowing the hand-written form is still worth it — it’s how you read what Lombok did, and how you add rules to build().

One use of this pattern earns a special mention: test-data builders. Tests need Order objects constantly, but each test cares about one detail. A builder with test-friendly defaults —

Order order = TestOrders.anOrder().giftWrap().build();   // everything else: sensible defaults

— keeps every test readable as a sentence about its detail, and when Order‘s constructor changes, you fix one builder instead of four hundred tests. Python’s factory_boy and JavaScript’s Fishery are whole libraries built on exactly this idea.

Where the pattern shows up is a fingerprint of the language, not of the problem. Rust has no named arguments, and its standard library chains Command::new("ls").arg("-l").spawn(); Go has the same gap and grew its famous functional-options idiom to cover it. Python and Kotlin, which can name every argument, barely use the pattern at all:

Which languages need Builder — and which get it for free

Builder is, in large part, Java’s answer to two missing language features: named arguments and default values. Where a language has them, the readability half of the pattern evaporates — and where it doesn’t, builders get reinvented, every time. The famous four:

order = Order(
    customer_id="cust-42",
    lines=[line],
    gift_wrap=True,       # named — no boolean soup
)                         # currency defaults to "USD" — no telescoping
  • Python (above) — keyword arguments and defaults are built in, and a dataclass with __post_init__ even covers the build-time validation. Builders here are imported Java habit, not need. (Kotlin is the same story — named parameters plus default values — which is why idiomatic Kotlin almost never has builders while Java is full of them.)
  • JavaScript / TypeScript — the options object is the ecosystem’s named arguments: createOrder({ customerId, giftWrap: true }), with defaults filled by destructuring — function createOrder({ currency = "USD", giftWrap = false }).
  • Rust — proves the rule from the other side: no named or default arguments, so builders are standard-library idiom — Command::new("ls").arg("-l").spawn() — and half the crates on crates.io ship a SomethingBuilder.
  • Go — same gap, different cure: the famous “functional options” pattern, NewServer(WithTimeout(5*time.Second), WithTLS(cert)) — a builder wearing a list of option functions.

This is the useful way to think about any pattern: a pattern is a workaround for something the language can’t say directly. When a language learns to say it, the pattern quietly evaporates there — and where the gap stays open, the pattern re-grows on its own, uninvited. (Java’s record types shrink the need too, but named arguments still don’t exist, so builders live on.)

Builders shipped with every SDK#

  • StringBuilder, HttpRequest.newBuilder(), Stream.builder() in the JDK; OkHttp’s Request.Builder; protobuf’s generated builders; Spring Security’s http.authorizeHttpRequests(...)... configuration chain.
  • Query builders everywhere: Java’s jOOQ, Node’s Knex (knex('orders').where('status', 'PAID').limit(10)), Python SQLAlchemy’s select(...).where(...) — assembling a complex SQL statement step by step is the pattern at its most natural.

When NOT to use it#

  • Two or three fields, all required. A constructor (or a record) says that better and shorter. Builders earn their keep around the fourth optional field.
  • The object is mutable anyway. If the thing has setters and changes freely after creation, a builder in front of it is decoration — the “can’t exist half-made” guarantee was the point.

04 · Singleton — one instance for the whole app#

Some objects in an app are shared infrastructure: the database connection, the logger writing to one file, the cache client, the thread pool. The whole app needs exactly one of each. And that need always gets discovered the same way — a new developer doesn’t know the object is supposed to be shared, and writes the obvious thing:

public class OrderRepository {
    private final Database db = new Database("jdbc:postgresql://prod-db:5432/shop", user, pw);
}

It works, so it spreads — every new class that touches the database copies the same line:

public class InvoiceRepository {
    private final Database db = new Database("jdbc:postgresql://prod-db:5432/shop", user, pw);
}

public class ReportJob {
    private final Database db = new Database("jdbc:postgresql://prod-db:5432/shop", user, pw);
}

public class CustomerSearch {
    private final Database db = new Database("jdbc:postgresql://prod-db:5432/shop", user, pw);
}

Nobody ever decided the app should hold dozens of database connections — they accumulated, one copy-pasted new at a time. And each one is a real TCP connection with a real login handshake on the other end. Postgres refuses connection number 101 by default, so one busy afternoon the log fills with FATAL: sorry, too many clients already. The logger version of the same mistake is quieter: five logger instances writing to one file, interleaving half-written lines over each other.

Singleton is the fix everyone finds first — for most people it’s the only pattern they’ve used for years without knowing the others exist. The move: the class itself refuses to be constructed twice.

public final class Database {
    private static final Database INSTANCE = new Database();

    public static Database getInstance() { return INSTANCE; }

    private Database() { /* open the one real connection */ }
}

The whole trick is that one word private on the constructor. Constructors are normally public — that’s what makes new Database() legal from any file. Marking it private means only code inside the Database class itself can call it, so the only new Database() the compiler will ever accept is the one on the INSTANCE line. Everywhere else it’s a compile error — the oneness isn’t a team convention someone can forget, the compiler enforces it. Every class calls Database.getInstance() instead, and every caller gets back the same object, whether they realize it or not. The too-many-clients bug is dead: a hundred call sites, one connection.

each class opens its ownOrderRepositoryInvoiceRepositoryReportJobPostgresnew Database(…)new Database(…)new Database(…)3 objects, 3 connections — and countingFATAL: sorry, too many clients alreadyeveryone shares the one instanceOrderRepositoryInvoiceRepositoryReportJobDatabasethe one instancePostgresgetInstance()getInstance()getInstance()1 conna hundred call sites — one object, one connection
What the pattern buys. Left: every class opens its own connection, and the count only ever grows. Right: the private constructor forces every call site through getInstance(), so they all share the one object — and Postgres sees a single connection.

The dangerous half — four ways getInstance() bites#

For that bug, the pattern genuinely works — no shame in having shipped it for years. But this pattern is exactly as dangerous as it is useful, and the danger hides in the very property that looked like the whole point: any code, anywhere, can write Database.getInstance() and it works. That reach-from-anywhere convenience is the damage, four ways:

  • Dependencies go invisible. new OrderRepository() looks self-contained — no signature anywhere admits it needs a database, because the reach for Database.getInstance() happens inside. Hidden dependencies get discovered one stack trace at a time. Miško Hevery’s famous name for this: “Singletons are Pathological Liars.”
  • Tests can’t swap it. getInstance() is welded to the one real connection, so a unit test for OrderRepository needs a live Postgres — there’s no seam to hand in an in-memory fake. Teams end up adding setInstanceForTesting(...) backdoors, which is the class admitting the design failed.
  • It’s global mutable state. Whatever any caller does to the shared object — opens a transaction, flips a session setting — every other caller feels it, with no warning in any signature. Every lesson about global variables applies; getInstance() is a global wearing a pattern’s name.
  • Oneness gets compiled in. The day the app needs a second database — a read replica, a second tenant, a throwaway instance per test — the class physically can’t hold two. “How many exist” stopped being a decision anyone can make.

The third one is the sneakiest, so it deserves a scene. ReportJob needs to stream a huge result, so it flips a setting on the connection:

Database.getInstance().setAutoCommit(false);   // "hold everything until I commit"

In a different file, written by a different person, OrderRepository marks an order paid:

Database.getInstance().execute("UPDATE orders SET status = 'PAID' WHERE id = 42");

OrderRepository believes that update is saved — autocommit normally saves every statement immediately. But this is the same object ReportJob just modified, so autocommit is now off and the update silently sits uncommitted. Order 42 stays unpaid. Two pieces of code that have never heard of each other just broke each other — and reading OrderRepository will never find the bug, because the cause lives in a file nobody had a reason to open.

For a database specifically, production apps solve the shared-mutable-state part with one more move: share a connection pool, not a raw connection (HikariCP is the Java default — 10 connections out of the box). Each piece of work borrows a connection, uses it, and returns it — ReportJob’s transaction lives on its borrowed connection and touches nobody else’s, while the total count stays capped, never 101. The oneness didn’t disappear; it moved up a level: the pool is the thing there’s exactly one of.

All four bullets are the same question answered in the wrong place: who decides how many Database objects exist? With getInstance(), the class itself decides — “only one, ever” is a law compiled into it. But the right count depends on the situation: production wants one, a unit test wants a fake, tomorrow’s read replica makes it two. Only the code assembling the app knows which situation it’s in — so the count belongs there, in the wiring, not inside the class.


Dependency injection — the same oneness, enforced in the wiring#

The fix: stop making Database guard itself. Make it a plain class again, create it once in main when the app starts, and pass that one object into the constructor of every class that needs it. The app still has exactly one instance — but for the same reason your kitchen has one pot of coffee this morning: you brewed one. Not because the machine physically prevents a second pot. That’s the whole of dependency injection: classes don’t go fetch what they need — they’re handed it.

The name is grander than the thing. A dependency is anything a class needs to do its job — OrderRepository needs a Database, the same way OrderService needed a mailer at the top of this post. Injection means the class doesn’t create or fetch that thing itself — it declares it as a constructor parameter, and whoever is building the class supplies it from outside. That’s the entire mechanism: a constructor parameter, taken seriously.

The intuition worth keeping: a class should read like a recipe that lists its ingredients up front — OrderRepository(Database db) announces “I need a database to work.” getInstance() is the recipe that secretly walks to the pantry mid-cooking and grabs whatever’s there: the dish still gets made, but nobody reading the recipe knows the pantry was involved, and nobody can hand it different ingredients.

Run the three situations through both answers to “who decides the count”:

SituationThe class decides (getInstance())The wiring decides (create once in main)
Production — need oneone instance ✓main creates one ✓
Unit test — need a fakeimpossible: welded to the real onethe test calls new FakeDatabase()
Read replica tomorrow — need twoimpossible: the law says onemain creates two ✓

Same result in the only case the singleton handles — and freedom in the two cases where it breaks. When a test needs its own instance, it just makes one; nothing forbids it, because the forbidding was the disease.

public class Database {
    private final Connection conn;

    public Database(String url, String user, String pw) { this.conn = open(url, user, pw); }
}

public class OrderRepository {
    private final Database db;

    public OrderRepository(Database db) { this.db = db; }   // visible, swappable

    public Order find(long id) { /* uses db — and the signature says so */ }
}

And the wiring itself — main, the one place that decides the count:

public static void main(String[] args) {
    Database db = new Database(config.dbUrl(), config.user(), config.pw());   // brewed once

    OrderRepository orders = new OrderRepository(db);
    InvoiceRepository invoices = new InvoiceRepository(db);
    ReportJob reports = new ReportJob(db);
    // ...app runs — nobody else ever calls new Database()
}

Which unlocks the test the dangerous-half section said was impossible:

@Test
void findsAnOrder() {
    Database fake = new FakeDatabase();                  // subclass answering from a HashMap
    OrderRepository repo = new OrderRepository(fake);    // handed in — no Postgres anywhere

    assertEquals("PAID", repo.find(42).status());
}
getInstance(): hidden dependencyOrderRepository( )InvoiceRepository( )ReportJob( )Database.getInstance()empty constructors — the signatures lieinjected: visible dependencyDatabaseone instance, made onceOrderRepository(Database)InvoiceRepository(Database)ReportJob(Database)handed in once — the signatures tell the truth
The same single instance, two wirings. Left: empty constructors while every class secretly reaches for the global — the signatures lie. Right: the one instance is handed in through constructors — every dependency is visible in the signature, and a test can hand in a fake.

One instance still exists — created once at startup and passed down, either by hand in main or by a DI container (a framework whose whole job is building your objects and handing them their dependencies). In Spring, every @Service and @Bean is singleton-scoped by default: one instance, managed, visible in constructors, swappable in tests. You’ve been using the singleton pattern correctly every day — the container took over the getInstance() job so your classes didn’t have to.

The same resolution exists in every ecosystem, which says something about how universal the lesson is: NestJS providers (Node) are singleton-scoped by default and constructor-injected; FastAPI‘s Depends(...) (Python) does the handing-in at request time; and both Node and Python have a quieter singleton you use constantly — a module is loaded once and cached, so import config in Python or require("./db") in Node hands every caller the same instance. Fine for stateless things; the moment it holds mutable state or needs swapping in tests, the same hidden-dependency problems return, and the answer is the same: pass it in.


Which tool, then?#

flowchart TD
  Q1{Several possible classes<br/>behind one interface?} -->|yes| Q2{Do they come in<br/>matched families?}
  Q2 -->|yes| AF([Abstract Factory])
  Q2 -->|no| F([Factory])
  Q1 -->|no| Q3{Many optional parts —<br/>constructor going unreadable?}
  Q3 -->|yes| B([Builder])
  Q3 -->|no| Q4{Need exactly one,<br/>shared everywhere?}
  Q4 -->|yes| DI([plain class, created once,<br/>injected everywhere])
  Q4 -->|no| N([just call <b>new</b>])
The creational decision in one walk. Note the bottom-right escape hatch: most objects need none of this — new is not a code smell.

That completes the classic-patterns notes: behavioral, wrapping, creational. A few classics were left out on purpose — not because they’re bad, but because you’ll almost never write them by hand:

The patterns deliberately skipped, and why
  • Template Method — an abstract class with hook methods (setUp()/tearDown() style). You meet it inside frameworks constantly, but when writing your own code, Strategy with lambdas does the same job without inheritance.
  • Composite — a folder and a file sharing one interface so trees nest (the DOM, file systems). When you need a tree, you’ll build this without needing its name.
  • Mediator — a central coordinator so objects don’t talk directly. In practice it dissolved into event buses — Observer wearing a coordinator hat.
  • Prototype — create by cloning an existing object. Builders and copy constructors won this fight long ago.
  • Flyweight — share immutable objects instead of duplicating them (string interning, Integer caches). Languages and runtimes do this for you.
  • Memento — snapshot an object so you can restore it later. Command’s undo() covers the practical ground.
  • Iterator — hand items out one at a time. It won so completely that it became language syntax (for-each, generators, for...of) — you consume it daily and will almost never write one yourself.
  • Bridge — an abstraction holding an injected implementation. You already write this by instinct; it stopped needing a name.
  • Interpreter — build a mini-language and evaluate it. If you ever truly need one, you’ll be reaching for a parser library, not this pattern.

Worth recognizing on sight; rarely worth writing. If one of them ever shows up in your codebase, that’s the day to read about it properly.

From here it’s the distributed series — starting with the question the Observer section left hanging: how do you save to your database and announce an event, when the process can die between the two? The dual-write problem, next.

Comments

Signed in with GitHub. Be kind.