Creational Design Patterns: Practical Object Creation for Modern Codebases

You know the moment: a “simple” feature request lands, you add one more constructor argument, and suddenly half your codebase is passing undefined for parameters nobody understands anymore. Or you add a new provider (Stripe + Adyen, AWS + Azure, SMTP + SES), and now object creation logic is scattered across controllers, handlers, and cron jobs. In my experience, the problem usually isn’t the business logic—it’s that object creation has quietly become business logic.\n\nCreational design patterns are the tools I reach for when I want my system to stop caring about how objects are created, composed, and represented. They focus on object creation problems: hiding which concrete classes exist, centralizing assembly rules, and keeping configuration decisions from leaking everywhere. The payoff is real: fewer brittle constructors, fewer conditionals in “random” places, and a codebase where adding a new variant is mostly additive instead of invasive.\n\nI’ll walk through the five patterns I use most in modern codebases—Factory Method, Abstract Factory, Builder, Prototype, and Singleton—using realistic scenarios, runnable examples, and specific guidance on when to use them (and when you should not).\n\n## The Two Core Ideas That Make Creational Patterns Worth It\nCreational patterns usually succeed because they do two things consistently:\n\n1) They hide knowledge of specific classes.\n- Your application layer shouldn’t need to know whether it’s instantiating S3BlobStore or AzureBlobStore.\n- Your domain layer shouldn’t care whether an invoice exporter is PdfInvoiceExporter or HtmlInvoiceExporter.\n\n2) They hide the details of instantiation and assembly.\n- The order of steps matters (validate config, create dependencies, wire them together, apply defaults).\n- The logic for “how we build this thing” tends to be more complex than we admit, especially once environment flags, feature toggles, and security requirements enter the picture.\n\nA simple analogy I use with teams: if object creation is a restaurant kitchen, you don’t want every waiter cooking. You want waiters to place an order (an interface), and the kitchen to produce the dish (a concrete instance) based on the current menu (configuration) and constraints (policies).\n\n## Pattern Map: How I Decide Which One to Reach For\nI don’t pick patterns by name first. I pick them based on the shape of the pain.\n\n- If you keep writing if (type === ‘x‘) return new X() everywhere → Factory Method.\n- If you need multiple related objects that must match (AWS queue + AWS blob store) → Abstract Factory.\n- If constructing one object takes many steps and has multiple valid representations → Builder.\n- If “new” is expensive and you mostly want copies with small tweaks → Prototype.\n- If you truly need one shared instance (and you accept the trade) → Singleton.\n\nHere’s how this tends to look in 2026-era workflows:\n\n

Concern

Traditional Approach

2026-Friendly Approach

\n

\n

Object creation sprinkled around

Constructors called directly in handlers

Dedicated factories + dependency injection wiring

\n

Environment-based variants

if/else inside business logic

Factory chooses implementation at the edge

\n

Complex configuration objects

One huge constructor with 12 args

Builder with defaults + validation

\n

Expensive setup

Recompute every time

Prototype base + clone and patch

\n

Shared services

Global variable

Explicit singleton boundary or container-managed lifetime

\n\nIf you’re using a DI container (common in TypeScript, Kotlin, C#, Java), you’ll still see these patterns—just expressed differently. A container can host a factory method, or act like an abstract factory for a whole product family. The patterns don’t disappear; they become cleaner.\n\n## Factory Method: One Override Point Beats 12 Conditionals\nFactory Method is my “first stop” pattern. It’s useful when you want to separate what you do from what you instantiate, and you want subclasses (or pluggable implementations) to decide which concrete object to create.\n\nWhen I use it:\n- A class can’t anticipate which concrete type it must create.\n- I want a subclass to specify which object it creates.\n- I want to delegate object creation to helpers and keep the knowledge localized.\n\n### Runnable Example (Node.js): Exporting Invoices\nScenario: your system exports invoices. Some tenants want PDF, others want HTML. The export pipeline is stable; the writer varies.\n\n // factory-method-invoice-export.js\n\n class InvoiceWriter {\n write(invoice) {\n throw new Error(‘Not implemented‘);\n }\n }\n\n class PdfInvoiceWriter extends InvoiceWriter {\n write(invoice) {\n // Pretend this calls a PDF renderer.\n return PDF(INVOICE:${invoice.id}, TOTAL:${invoice.totalCents} cents);\n }\n }\n\n class HtmlInvoiceWriter extends InvoiceWriter {\n write(invoice) {\n return

Invoice ${invoice.id}

Total: ${invoice.totalCents} cents

;\n }\n }\n\n class InvoiceExporter {\n // Factory Method: subclasses decide which writer to create.\n createWriter() {\n throw new Error(‘Not implemented‘);\n }\n\n export(invoice) {\n const writer = this.createWriter();\n const output = writer.write(invoice);\n\n // Shared algorithm: audit and return.\n console.log([audit] exported invoice=${invoice.id} using writer=${writer.constructor.name});\n return output;\n }\n }\n\n class PdfInvoiceExporter extends InvoiceExporter {\n createWriter() {\n return new PdfInvoiceWriter();\n }\n }\n\n class HtmlInvoiceExporter extends InvoiceExporter {\n createWriter() {\n return new HtmlInvoiceWriter();\n }\n }\n\n // Demo\n const invoice = { id: ‘INV-2026-00172‘, totalCents: 12500 };\n\n const pdfExporter = new PdfInvoiceExporter();\n console.log(pdfExporter.export(invoice));\n\n const htmlExporter = new HtmlInvoiceExporter();\n console.log(htmlExporter.export(invoice));\n\nWhy this works well:\n- The creation decision is localized (createWriter).\n- The export algorithm stays consistent (export).\n- Adding a new format is additive: create a writer + exporter subclass.\n\nWhen I would not use Factory Method:\n- The only difference is a simple configuration value (no behavioral differences). A parameter is simpler.\n- You’ll end up with a subclass explosion (30 exporters). In that case, prefer a registry-based factory function or dependency injection configuration.\n\nCommon mistakes:\n- Putting business logic inside the factory method. Keep it about creation.\n- Creating a new expensive dependency every call when you meant to reuse it.\n\n### Practical Upgrade: Factory Method Without the Subclass Explosion\nIn JavaScript/TypeScript codebases, subclassing can feel heavy. A common “modern” approach is to keep the spirit of Factory Method (one override point) but implement it as a function parameter or injected strategy.\n\nI’ll often refactor the earlier example into a single exporter that accepts a writer factory:\n\n // factory-method-functional.js\n\n class InvoiceExporter {\n constructor(createWriter) {\n this.createWriter = createWriter;\n }\n\n export(invoice) {\n const writer = this.createWriter();\n const output = writer.write(invoice);\n console.log([audit] exported invoice=${invoice.id} using writer=${writer.constructor.name});\n return output;\n }\n }\n\n // Usage\n const exporter = new InvoiceExporter(() => ({\n write(inv) {\n return TXT(INVOICE:${inv.id});\n }\n }));\n\n console.log(exporter.export({ id: ‘INV-1‘, totalCents: 1000 }));\n\nSame pattern, less inheritance. The key is still there: call sites don’t assemble the object graph; the factory does.\n\n## Abstract Factory: Keeping Product Families Consistent\nAbstract Factory is Factory Method’s bigger cousin. Instead of creating one product, you create families of related products that must be used together.\n\nI reach for it when:\n- The system should be independent of how products are created and composed.\n- I need to support multiple families (AWS vs Azure) without mixing them.\n- Related products are designed to be used together and I want to enforce that.\n- I want to reveal interfaces, not implementations.\n\n### Runnable Example (Node.js): Cloud “Family” (Blob Store + Queue)\nScenario: you support two deployment environments. One uses AWS (S3 + SQS), another uses Azure (Blob Storage + Service Bus). You must not accidentally combine S3 with Service Bus.\n\n // abstract-factory-cloud-family.js\n\n class BlobStore {\n putObject(key, payload) {\n throw new Error(‘Not implemented‘);\n }\n }\n\n class Queue {\n sendMessage(topic, payload) {\n throw new Error(‘Not implemented‘);\n }\n }\n\n // AWS family\n class S3BlobStore extends BlobStore {\n putObject(key, payload) {\n console.log([s3] put key=${key} bytes=${payload.length});\n }\n }\n\n class SqsQueue extends Queue {\n sendMessage(topic, payload) {\n console.log([sqs] topic=${topic} payload=${JSON.stringify(payload)});\n }\n }\n\n // Azure family\n class AzureBlobStore extends BlobStore {\n putObject(key, payload) {\n console.log([azure-blob] put key=${key} bytes=${payload.length});\n }\n }\n\n class ServiceBusQueue extends Queue {\n sendMessage(topic, payload) {\n console.log([service-bus] topic=${topic} payload=${JSON.stringify(payload)});\n }\n }\n\n // Abstract Factory\n class CloudStackFactory {\n createBlobStore() {\n throw new Error(‘Not implemented‘);\n }\n\n createQueue() {\n throw new Error(‘Not implemented‘);\n }\n }\n\n class AwsCloudStackFactory extends CloudStackFactory {\n createBlobStore() {\n return new S3BlobStore();\n }\n\n createQueue() {\n return new SqsQueue();\n }\n }\n\n class AzureCloudStackFactory extends CloudStackFactory {\n createBlobStore() {\n return new AzureBlobStore();\n }\n\n createQueue() {\n return new ServiceBusQueue();\n }\n }\n\n class DocumentIngestService {\n constructor(cloudFactory) {\n this.blobStore = cloudFactory.createBlobStore();\n this.queue = cloudFactory.createQueue();\n }\n\n ingest(documentId, content) {\n const key = documents/${documentId}.txt;\n this.blobStore.putObject(key, Buffer.from(content, ‘utf8‘));\n\n this.queue.sendMessage(‘document-ingested‘, { documentId, key });\n }\n }\n\n function makeCloudFactoryFromEnv() {\n const provider = process.env.CLOUDPROVIDER

‘aws‘;\n if (provider === ‘aws‘) return new AwsCloudStackFactory();\n if (provider === ‘azure‘) return new AzureCloudStackFactory();\n throw new Error(Unsupported CLOUDPROVIDER=${provider});\n }\n\n // Demo\n const factory = makeCloudFactoryFromEnv();\n const service = new DocumentIngestService(factory);\n service.ingest(‘DOC-88421‘, ‘Quarterly compliance report‘);\n\nWhat I like about this approach:\n- The “family” is chosen once at the boundary.\n- The rest of the system sees BlobStore and Queue, not provider details.\n- You avoid invalid combinations by construction.\n\nWhen I would not use Abstract Factory:\n- You only have one product type; Factory Method or a simple factory function is enough.\n- You don’t actually need families—your “related objects” don’t have consistency constraints.\n\nCommon mistakes:\n- Making the abstract factory too big (15+ product types). If that happens, split it into smaller factories by domain area (messaging, storage, auth).\n- Hiding too much: sometimes you do need provider-specific tuning. I usually expose those through configuration objects, not type checks.\n\n### Edge Case: When the “Family” Is Really a Compatibility Matrix\nReal systems don’t always fit neatly into “AWS vs Azure.” Sometimes you have a matrix of concerns:\n- Storage: S3, Azure Blob, MinIO\n- Queue: SQS, Service Bus, RabbitMQ\n- Auth: IAM role, connection string, mTLS\n\nIf every combination is allowed, you don’t have a product family—you have independent components. In that world, Abstract Factory can become awkward because it forces bundling choices that aren’t actually coupled.\n\nWhat I do instead: create smaller factories per capability and let a composition root wire them together. The pattern shifts from “one big abstract factory” to “a few tight factories + one composer.” I still keep the same goal: choose implementations at the boundary, not inside business logic.\n\n## Builder: Turning “Constructor Soup” Into a Readable Assembly Line\nBuilder is the pattern I use when an object is complex, has many optional parts, must enforce invariants, and may have different final representations.\n\nWhen I use it:\n- The algorithm for creating a complex object should be independent of its parts.\n- Construction must allow different representations.\n\nA modern clue you need Builder: you have a constructor call that looks like a legal contract.\n\n### Runnable Example (Node.js): Building a Report Pipeline Config\nScenario: You generate reports. Some reports require PII redaction, some require caching, some require multiple destinations. A single config object gets gnarly.\n\n // builder-report-pipeline.js\n\n class ReportPipeline {\n constructor({ source, transforms, destinations, cacheSeconds }) {\n this.source = source;\n this.transforms = transforms;\n this.destinations = destinations;\n this.cacheSeconds = cacheSeconds;\n\n if (!this.source) throw new Error(‘source is required‘);\n if (!Array.isArray(this.destinations) this.destinations.length === 0) {\n throw new Error(‘at least one destination is required‘);\n }\n }\n\n run() {\n console.log([pipeline] source=${this.source});\n console.log([pipeline] transforms=${this.transforms.map(t => t.name).join(‘, ‘) ‘(none)‘});\n console.log([pipeline] destinations=${this.destinations.map(d => d.name).join(‘, ‘)});\n console.log([pipeline] cacheSeconds=${this.cacheSeconds});\n }\n }\n\n class ReportPipelineBuilder {\n constructor() {\n this.source = null;\n this.transforms = [];\n this.destinations = [];\n this.cacheSeconds = 0;\n }\n\n fromSource(source) {\n this.source = source;\n return this;\n }\n\n addTransform(transform) {\n this.transforms.push(transform);\n return this;\n }\n\n addDestination(destination) {\n this.destinations.push(destination);\n return this;\n }\n\n withCacheSeconds(cacheSeconds) {\n this.cacheSeconds = cacheSeconds;\n return this;\n }\n\n build() {\n // Centralized validation and defaults live here.\n if (!this.source) throw new Error(‘fromSource(…) must be called‘);\n\n return new ReportPipeline({\n source: this.source,\n transforms: this.transforms,\n destinations: this.destinations,\n cacheSeconds: this.cacheSeconds,\n });\n }\n }\n\n // Some realistic parts\n const redactPii = { name: ‘redactPii‘ };\n const normalizeCurrency = { name: ‘normalizeCurrency‘ };\n const s3Destination = { name: ‘s3Destination‘ };\n const emailDestination = { name: ‘emailDestination‘ };\n\n // Demo\n const pipeline = new ReportPipelineBuilder()\n .fromSource(‘warehouse:dailysales‘)\n .addTransform(redactPii)\n .addTransform(normalizeCurrency)\n .addDestination(s3Destination)\n .addDestination(emailDestination)\n .withCacheSeconds(300)\n .build();\n\n pipeline.run();\n\nWhy Builder holds up in real code:\n- The call site reads like intent, not plumbing.\n- Defaults and validation are centralized.\n- You can offer multiple “pre-baked” builders (compliance report builder, finance report builder) without subclassing everything.\n\nWhen I would not use Builder:\n- The object has 2–3 fields and no invariants; a plain constructor or object literal is clearer.\n- You have many tiny builder classes that each build trivial objects—this is ceremony without value.\n\nPerformance note:\n- Builders typically add negligible overhead. In Node services I’ve profiled, the cost is usually dominated by I/O, parsing, or cryptography. The bigger risk is not CPU; it’s correctness (missing validation) and maintainability.\n\n### A Builder Trick I Use a Lot: “Freeze the Built Object”\nA subtle benefit of Builder is that it gives you a single point to enforce immutability. In JavaScript, mutability is one of the fastest paths to “why did this config change at runtime?”\n\nIf the object is meant to be a configuration or a definition, I’ll often freeze it after building:\n- Object.freeze(config) for shallow freezing\n- a small recursive freezer for deep objects (careful with cycles)\n\nThe goal isn’t to win an academic purity contest—it’s to make it harder for a random helper function to mutate something it shouldn’t. Builders make this much easier to do consistently because all objects pass through one place.\n\n## Prototype: Fast Copies When “New” Is the Expensive Part\nPrototype is for cases where creating a new instance from scratch is costly, and you can instead clone an existing object and modify it.\n\nI use it when:\n- Instance creation is expensive (deep graphs, computed defaults, compiled templates).\n- The classes to instantiate are chosen at runtime.\n- I want to avoid parallel factory hierarchies that mirror product hierarchies.\n\nA modern version of this pattern shows up in:\n- Request templates (base headers + auth + timeouts, then per-call changes)\n- UI schema or form models\n- Simulation configurations\n- Workflow definitions\n\n### Runnable Example (Node.js): Cloning Request Templates\nScenario: your service calls multiple upstream APIs. Each call shares a base config (timeouts, headers, retry policy). Building it repeatedly invites mistakes.\n\nThe important part is: you want a safe copy, not shared references. For plain JSON-ish data, a deep clone is straightforward. For rich objects (class instances, Dates, Maps), cloning is trickier and you usually want an explicit clone() method.\n\n // prototype-request-template.js\n\n function cloneJsonSafe(value) {\n // Good for plain data objects (no functions, no Dates, no Maps).\n // Avoids shared references between templates and instances.\n return JSON.parse(JSON.stringify(value));\n }\n\n class ApiRequestTemplate {\n constructor({ baseUrl, headers, timeoutMs, retry }) {\n this.baseUrl = baseUrl;\n this.headers = headers;\n this.timeoutMs = timeoutMs;\n this.retry = retry;\n\n if (!this.baseUrl) throw new Error(‘baseUrl is required‘);\n if (!this.headers) throw new Error(‘headers is required‘);\n }\n\n clone() {\n // Explicit clone: safe and intentional.\n return new ApiRequestTemplate({\n baseUrl: this.baseUrl,\n headers: cloneJsonSafe(this.headers),\n timeoutMs: this.timeoutMs,\n retry: cloneJsonSafe(this.retry),\n });\n }\n\n withPatch(patch) {\n // Return a modified clone (immutable-style).\n const cloned = this.clone();\n if (patch.baseUrl) cloned.baseUrl = patch.baseUrl;\n if (patch.timeoutMs != null) cloned.timeoutMs = patch.timeoutMs;\n if (patch.headers) cloned.headers = { …cloned.headers, …patch.headers };\n if (patch.retry) cloned.retry = { …cloned.retry, …patch.retry };\n return cloned;\n }\n\n toRequest(path, options) {\n const req = {\n method: options?.method ‘GET‘,\n url: ${this.baseUrl}${path},\n timeoutMs: options?.timeoutMs ?? this.timeoutMs,\n headers: { …this.headers, …(options?.headers {}) },\n retry: { …this.retry, …(options?.retry {}) },\n body: options?.body ?? null\n };\n\n // Lightweight guardrails\n if (!req.headers[‘x-request-id‘]) {\n throw new Error(‘x-request-id header is required for tracing‘);\n }\n\n return req;\n }\n }\n\n // Base prototype\n const baseTemplate = new ApiRequestTemplate({\n baseUrl: ‘https://api.partner.example‘,\n headers: {\n ‘user-agent‘: ‘billing-service/1.0‘,\n ‘content-type‘: ‘application/json‘\n },\n timeoutMs: 2000,\n retry: { attempts: 2, backoffMs: 200 }\n });\n\n // Clone + patch for Partner A\n const partnerATemplate = baseTemplate.withPatch({\n headers: { ‘x-partner‘: ‘partner-a‘ },\n timeoutMs: 3000\n });\n\n // Clone + patch for Partner B\n const partnerBTemplate = baseTemplate.withPatch({\n headers: { ‘x-partner‘: ‘partner-b‘ },\n retry: { attempts: 4 }\n });\n\n // Demo requests\n const requestA = partnerATemplate.toRequest(‘/invoices‘, {\n method: ‘POST‘,\n headers: { ‘x-request-id‘: ‘req-1001‘ },\n body: { invoiceId: ‘INV-9‘ }\n });\n\n const requestB = partnerBTemplate.toRequest(‘/status‘, {\n headers: { ‘x-request-id‘: ‘req-1002‘ }\n });\n\n console.log(requestA);\n console.log(requestB);\n\nWhy this is Prototype in practice:\n- You build one carefully-reviewed “base” object.\n- You clone it cheaply and safely.\n- You only tweak what changes per variant.\n\n### Prototype Pitfall: “Clone” That Still Shares References\nThe number one bug I see with Prototype in JavaScript is accidental sharing:\n- you clone the top-level object\n- but nested objects/arrays still point to the original\n\nThen a single tweak (say, adding a header) mutates the prototype and impacts every future request. If your clones are plain objects, use a deep clone method. If you have class instances, prefer an explicit clone() that knows exactly which fields must be copied.\n\n### Prototype vs Factory: The Rule of Thumb I Use\n- If the base setup is expensive or highly repetitive → Prototype (clone + patch).\n- If the base setup is cheap but the decision logic is complex → Factory (choose and construct).\n\nThey’re not enemies. In some systems, the factory returns a prototype which you then clone per request. The point is to keep object creation consistent and intentional.\n\n## Singleton: One Instance, On Purpose (With Eyes Open)\nSingleton is the most controversial creational pattern, and for good reason. It gives you a globally accessible single instance. That can simplify access to a shared resource—but it can also hide dependencies, complicate tests, and create lifecycle problems.\n\nWhen I use it (rarely, but yes, sometimes):\n- There is truly one logical instance per process (metrics registry, process-wide config snapshot, logger).\n- The instance is expensive and shared by design (a connection pool manager in some environments).\n- I can clearly define lifecycle boundaries (init once, close on shutdown).\n\nWhen I avoid it:\n- When it’s used as “dependency injection, but lazier.”\n- When test isolation matters and the singleton retains state between tests.\n- When the app runs in multiple processes/containers and people assume singleton means “globally unique across the cluster.” (It doesn’t.)\n\n### Runnable Example (Node.js): A Process-Scoped Metrics Registry\nScenario: you want one metrics registry per process. You want to avoid multiple registries that double-count. You also want tests to be able to reset it.\n\n // singleton-metrics-registry.js\n\n class MetricsRegistry {\n constructor() {\n this.counters = new Map();\n }\n\n inc(name, value = 1) {\n const current = this.counters.get(name)

0;\n this.counters.set(name, current + value);\n }\n\n snapshot() {\n return Object.fromEntries(this.counters.entries());\n }\n\n resetForTests() {\n this.counters.clear();\n }\n }\n\n const Metrics = (() => {\n let instance = null;\n\n return {\n getInstance() {\n if (!instance) instance = new MetricsRegistry();\n return instance;\n },\n // Optional: allow tests to reset\n reset() {\n instance = null;\n }\n };\n })();\n\n // Demo\n const metrics1 = Metrics.getInstance();\n const metrics2 = Metrics.getInstance();\n\n metrics1.inc(‘requeststotal‘);\n metrics2.inc(‘requeststotal‘, 2);\n\n console.log(metrics1 === metrics2);\n console.log(metrics1.snapshot());\n\nA few practical notes:\n- This is one singleton per Node process. If you run 10 containers, you have 10 instances.\n- The reset() escape hatch is intentionally ugly—it signals “don’t use this in production code paths,” but it keeps unit tests sane.\n- If you already have a DI container, it’s usually cleaner to let the container manage singleton lifecycle rather than using a static/global accessor.\n\n### Singleton Smell Test\nWhen someone proposes a singleton, I ask three questions:\n1) “Is this truly a single instance per process, by design?”\n2) “What happens in tests—does state leak across test cases?”\n3) “What happens on shutdown—do we need to close resources?”\n\nIf any of those answers are unclear, I treat singleton as a red flag and look for alternatives: pass the dependency explicitly, use container-managed lifetime, or make the shared thing a pure function with explicit inputs.\n\n## The “Simple Factory” (Not One of the Five, But You’ll Use It)\nIn real codebases, I often start with a simple factory function before I reach for more formal patterns. It’s not always listed as one of the classic patterns, but it’s incredibly practical.\n\nA simple factory is just a function that returns an implementation based on input. It’s the smallest step away from scattered new calls.\n\n // simple-factory-payment-gateway.js\n\n class StripeGateway {\n charge(cents) {\n return stripe:charged:${cents};\n }\n }\n\n class AdyenGateway {\n charge(cents) {\n return adyen:charged:${cents};\n }\n }\n\n function makePaymentGateway({ provider }) {\n if (provider === ‘stripe‘) return new StripeGateway();\n if (provider === ‘adyen‘) return new AdyenGateway();\n throw new Error(Unknown provider=${provider});\n }\n\nThis becomes Factory Method or Abstract Factory only when you need scaling properties that the simple factory doesn’t give you (test seams, subclassing/overrides, families, or more formal contracts).\n\n## How Creational Patterns Fit Into Modern Dependency Injection\nIf you use dependency injection (DI), the “creation problem” doesn’t go away—it moves. And that’s good, because DI creates a natural place to centralize assembly: the composition root (startup wiring).\n\nHere’s how I map the patterns into DI-heavy codebases:\n- Factory Method: a component depends on a createX() function (or provider) rather than calling new X()\n- Abstract Factory: a component depends on a CloudStackFactory interface and asks it for multiple related dependencies\n- Builder: build complex configuration/value objects before injecting them\n- Prototype: register a base template and clone per request\n- Singleton: let the container manage “one instance,” rather than hiding it behind globals\n\n### Boundary Rule I Follow\nCreation decisions should be made at boundaries:\n- process startup\n- request entrypoint\n- job runner entrypoint\n- CLI command entrypoint\n\nOnce you’re inside domain logic, you want to deal in stable abstractions (BlobStore, Queue, PaymentGateway) and avoid branching on environment or provider. Creational patterns help enforce that rule.\n\n## Testing Creational Code Without Losing Your Mind\nA nice side effect of creational patterns is better testing—if you use them intentionally. A few tactics I rely on:\n\n### 1) Test the Factory Separately\nFactories often encode important policy decisions (“in production, use AWS; in dev, use local emulator”). I treat them as first-class code with their own tests.\n\nWhat to validate:\n- it selects the correct implementation for each config\n- it rejects invalid configs with helpful errors\n- it wires dependencies correctly (at least at the “shape” level)\n\n### 2) Prefer Constructor Injection for Business Logic\nEven if you’re not using a full DI container, constructor injection makes your services test-friendly:\n- production code uses real factory outputs\n- tests pass fakes/mocks directly\n\nThat avoids the “how do I mock a singleton?” spiral.\n\n### 3) For Singletons, Provide a Reset Hook (Or Don’t Use Them)\nIf a singleton holds state, tests will eventually fail in weird orders. Either:\n- don’t store mutable state in the singleton\n- or provide a clear reset mechanism\n- or avoid singleton entirely and inject the instance\n\nIn practice, if a singleton needs a reset hook, that’s a sign it might be better expressed as container-managed lifecycle or an explicit dependency passed around.\n\n## Performance and Operational Considerations (The Stuff That Bites in Production)\nCreational patterns aren’t just about aesthetics. They can change performance and reliability in predictable ways.\n\n### Avoid Accidental Per-Request Heavy Instantiation\nA common bug looks like this:\n- a factory method is called inside a request handler\n- it creates a new SDK client, loads certificates, or compiles templates\n- latency spikes and memory churn grows\n\nIf an object is expensive and safe to reuse, consider:\n- creating it once at startup and injecting it\n- caching it inside the factory (with careful attention to config scoping)\n- using Prototype: build once, clone per request when needed\n\n### Be Explicit About Lifecycle\nFor anything that holds resources (sockets, file handles, timers), I want an explicit lifecycle plan:\n- where it’s created\n- where it’s closed\n- what happens on shutdown\n\nCreational patterns make this easier because creation is centralized; don’t waste that benefit by leaving shutdown scattered.\n\n### Prefer “Configuration Objects” Over Type Checks\nWhen you need provider-specific tuning, avoid instanceof checks in business logic. I’d rather:\n- define a shared interface\n- pass provider-specific settings via configuration\n- keep branching inside the factory or adapter\n\nThat keeps your domain logic stable even as providers change.\n\n## Common Pitfalls Across All Creational Patterns\nThese show up so often that I treat them like a checklist.\n\n### Pitfall 1: Turning Factories Into Mini-Frameworks\nFactories should create and wire objects. When a factory starts doing work that belongs in the object itself (like processing invoices or performing I/O), you’ve blurred responsibilities.\n\nA simple guardrail I use: the factory is allowed to validate configuration and build dependencies, but it shouldn’t perform domain actions.\n\n### Pitfall 2: Hiding Errors Until Runtime\nIf object creation depends on environment variables, feature flags, or secrets, failures can become late and confusing. Central creation helps, but only if you use it to fail fast.\n\nI like factories that:\n- validate config at startup\n- throw errors with actionable messages\n- include which setting caused the failure\n\n### Pitfall 3: Over-Abstraction Too Early\nPatterns are tools, not trophies. If you only have one concrete implementation and no realistic plan for a second, adding an interface/factory layer might be premature.\n\nMy rule: abstract when there is real variability or real pain, not because variability is “possible.” Almost everything is possible.\n\n### Pitfall 4: Making Builders Mutate After Build\nBuilders work best when “build” creates a finished product that won’t be mutated later. If the built object keeps changing, invariants become blurry.\n\nIf mutation is required, consider:\n- exposing explicit methods on the object that re-validate invariants\n- using immutable updates (withX(...) returns a copy)\n- separating “definition” from “execution state”\n\n## A Refactoring Playbook: Introducing Creational Patterns Without a Rewrite\nWhen I want to add creational patterns to an existing codebase, I do it in small, low-risk steps.\n\n### Step 1: Identify a Creation Hotspot\nLook for places where object creation is scattered:\n- repeated new calls of the same class\n- repeated conditional selection (if provider)\n- repeated “construct config then call client” boilerplate\n\n### Step 2: Create a Small Factory at the Edge\nStart with a simple factory function that returns the interface you want. Don’t try to pattern-match the textbook yet.\n\n### Step 3: Replace Call-Site new With Factory Calls\nKeep behavior identical. Your goal is to centralize creation, not “improve architecture” in the same commit.\n\n### Step 4: Split Into Abstract Factory / Builder / Prototype Only When Needed\nOnce creation is centralized, the right pattern often becomes obvious:\n- multiple related products → Abstract Factory\n- complex config/invariants → Builder\n- expensive base setup → Prototype\n\n### Step 5: Add Tests for the Creation Rules\nThe real value is encoding creation rules in a single place and proving them.\n\n## Quick Decision Checklist\nIf you’re deciding in the moment, here’s the shortest reliable checklist I know:\n\n- Do I keep selecting between implementations in many places? → Factory Method (or simple factory)\n- Do I need a consistent set of related implementations? → Abstract Factory\n- Do I have a complex object with many options and invariants? → Builder\n- Is creating the object expensive, and copies differ only slightly? → Prototype\n- Do I truly need one shared instance per process, with clear lifecycle? → Singleton (or container-managed singleton)\n\n## Closing Thought: Object Creation Is Architecture\nThe biggest shift for me was realizing that object creation is architecture. It’s where you encode what’s allowed, what’s compatible, what’s safe, and what changes when the environment changes. If you let creation logic sprawl, it silently becomes the hardest-to-change part of your system.\n\nCreational patterns are a way to reclaim that space: keep creation decisions explicit, keep variability at the edges, and keep the rest of your code focused on behavior. And when the next “simple” feature request lands, you’ll have fewer constructors turning into mysteries—and more places where adding a new variant is just adding code, not untangling it.

Scroll to Top