Factory: Encapsulating and Abstracting Object Creation
Starting from `new` calls scattered across a codebase, see how the Factory Pattern centralizes object creation logic — and learn to tell Simple Factory, Factory Method, and Abstract Factory apart.
What Code Looks Like Without a Factory
Let's start with an example that doesn't use any pattern at all. Say you need to send a message based on notification type:
class EmailNotification {
send(message) {
console.log(`Sending email: ${message}`)
}
}
class SmsNotification {
send(message) {
console.log(`Sending SMS: ${message}`)
}
}
class PushNotification {
send(message) {
console.log(`Sending push notification: ${message}`)
}
}
function notifyUser(type, message) {
let notification
if (type === 'email') {
notification = new EmailNotification()
} else if (type === 'sms') {
notification = new SmsNotification()
} else if (type === 'push') {
notification = new PushNotification()
} else {
throw new Error(`Unsupported notification type: ${type}`)
}
notification.send(message)
}If every other place that needs to send a notification has to repeat this same "which class do I new up for this type" logic, that if/else chain ends up copy-pasted throughout the codebase. Add a SlackNotification later, and you have to track down every copy and patch in the new branch — easy to miss one.
Simple Factory: Centralize the Decision Logic
The most obvious fix is to pull that decision logic into its own function, so everywhere else just calls the function without needing to know which class maps to which type:
class NotificationFactory {
static create(type) {
switch (type) {
case 'email':
return new EmailNotification()
case 'sms':
return new SmsNotification()
case 'push':
return new PushNotification()
default:
throw new Error(`Unsupported notification type: ${type}`)
}
}
}
function notifyUser(type, message) {
const notification = NotificationFactory.create(type)
notification.send(message)
}This is usually called a Simple Factory. Strictly speaking it's not one of the official patterns from the Gang of Four's book — it's more of a convention — but it already fixes the core problem: the if/else chain now lives in exactly one place, NotificationFactory.create(). Add a SlackNotification later, and that's the only spot you touch. Every caller stays untouched.
Factory Method: Let Subclasses Decide What to Create
Simple Factory centralizes the decision, but that switch statement still has to know about every subclass in existence. Once you have enough types, create() keeps bloating, and every new type means going back to edit that shared factory.
Factory Method takes a different angle: instead of one switch making the call, you define an interface for creating objects and let each subclass decide what to instantiate.
class NotificationSender {
send(message) {
const notification = this.createNotification() // delegated to the subclass
notification.send(message)
}
}
class EmailSender extends NotificationSender {
createNotification() {
return new EmailNotification()
}
}
class SmsSender extends NotificationSender {
createNotification() {
return new SmsNotification()
}
}
const emailSender = new EmailSender()
emailSender.send('Account verification email') // Sending email: Account verification email
const smsSender = new SmsSender()
smsSender.send('Verification code: 123456') // Sending SMS: Verification code: 123456NotificationSender has no idea what kind of notification object actually gets created — it only defines the "send a notification" flow. The decision of which notification to create is pushed down to subclasses through createNotification(). Adding a new delivery channel now just means adding a new subclass of NotificationSender, without touching any existing code. This is a textbook case of the Open/Closed Principle: open for extension, closed for modification.
Abstract Factory: Producing a Whole Family of Related Objects
When what you need to create isn't a single object but a whole family of objects that belong together, that's where Abstract Factory comes in.
Say your app needs to support both a light theme and a dark theme, and each theme has its own matching Button and Input components. You want the whole set of components to come from the same style once a theme is chosen — never mixed and matched:
class LightButton {
render() {
console.log('Rendering light button')
}
}
class LightInput {
render() {
console.log('Rendering light input')
}
}
class DarkButton {
render() {
console.log('Rendering dark button')
}
}
class DarkInput {
render() {
console.log('Rendering dark input')
}
}
const lightThemeFactory = {
createButton: () => new LightButton(),
createInput: () => new LightInput(),
}
const darkThemeFactory = {
createButton: () => new DarkButton(),
createInput: () => new DarkInput(),
}
function renderForm(themeFactory) {
const button = themeFactory.createButton()
const input = themeFactory.createInput()
button.render()
input.render()
}
renderForm(darkThemeFactory)
// Rendering dark button
// Rendering dark inputrenderForm has no idea whether it's dealing with the light or dark theme — it only relies on the interface themeFactory provides. As long as you pass in the same theme factory, the button and input it produces are guaranteed to match, so you never end up with a light button next to a dark input.
Telling the Three Apart in One Line Each
- Simple Factory: a single function that uses conditional logic to decide which class to
newup. The point is centralizing that decision. - Factory Method: object creation is defined as a method that subclasses override to decide what gets created. The point is letting you extend behavior without touching existing code.
- Abstract Factory: a whole set of creation methods bundled together, guaranteeing that objects coming from the same factory stay consistent with each other. The point is consistency across a whole family of related objects.
When to Use It, and When It's Overkill
It's worth reaching for one of these when:
- Object creation logic is genuinely complex, or needs to pick between different implementations based on conditions
- You expect to keep adding variants of the same kind of thing — new notification channels, new payment methods, and so on
- You want callers decoupled from concrete classes, so you can swap implementations later or mock them out in tests
If you're just creating a single object with nothing complicated going on, just new it directly. Don't apply a pattern for the sake of applying a pattern.
Wrapping Up
The core value of the Factory Pattern is separating "deciding what object to create" from "how that object gets used." Simple Factory centralizes the decision logic. Factory Method pushes that decision down to subclasses to satisfy the Open/Closed Principle. Abstract Factory goes a step further, guaranteeing that a whole family of related objects stays consistent with each other.