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
OrderServiceand nobody remembersReturnsService— 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
FreightCarrierand 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
newand 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.
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 writenew Express(). Same forBuffer.from(...)and nearly every “call the module, get an object” API in the ecosystem. - Python: classmethod constructors like
datetime.fromtimestamp(...)anddict.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,
newis honest and clear. Factories for everything is how you end up mocked in memes aboutFactoryFactoryProvider. - 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 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.
Matching sets in the wild#
- JDBC, quietly. A
ConnectioncreatesStatements, which createResultSets — 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"), thensession.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:
| Factory | Abstract Factory | |
|---|---|---|
| The question it answers | “which one?” | “which family?” |
| One call returns | one product | a matching set |
| The products are | competitors — freight or courier or postal | teammates — gateway and refunds and webhooks, same brand |
| The decision happens | per order, at runtime | once, at startup |
| The disease it kills | choice ladders copy-pasted into callers | mixed-brand sets that compile fine and break in production |
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:
customerIdis demanded to even start, andbuild()refuses to produce an order without lines or an address. The half-made object can’t escape —Orderitself has no setters, arrives complete, and never changes. - In real Java you rarely hand-write this: Lombok’s
@Buildergenerates 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 tobuild().
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
dataclasswith__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 aSomethingBuilder. - 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’sRequest.Builder; protobuf’s generated builders; Spring Security’shttp.authorizeHttpRequests(...)...configuration chain.- Query builders everywhere: Java’s jOOQ, Node’s Knex (
knex('orders').where('status', 'PAID').limit(10)), Python SQLAlchemy’sselect(...).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.
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 forDatabase.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 forOrderRepositoryneeds a live Postgres — there’s no seam to hand in an in-memory fake. Teams end up addingsetInstanceForTesting(...)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
Databaseguard itself. Make it a plain class again, create it once inmainwhen 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”:
| Situation | The class decides (getInstance()) | The wiring decides (create once in main) |
|---|---|---|
| Production — need one | one instance ✓ | main creates one ✓ |
| Unit test — need a fake | impossible: welded to the real one | the test calls new FakeDatabase() ✓ |
| Read replica tomorrow — need two | impossible: the law says one | main 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());
}
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>]) 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,
Integercaches). 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.