Notification System: Factory Pattern
Build a Java notification system using the Factory Method design pattern.
Introduction
30 Second Summary
Every app you use sends notifications through different channels. Email confirmations, text alerts, push notifications on your phone. Behind the scenes, developers need a clean way to add new channels without rewriting existing code.
In this project, you will build a notification system in Java that demonstrates the Factory Method Pattern. You'll start with tightly-coupled code, see the problems it creates, then refactor it into a clean, extensible architecture.
What You'll Build
A Java notification system that evolves through three stages: naive code, a simple factory, and the full Factory Method pattern with abstract creators and polymorphic dispatch.
By the end of this project, you'll have:
- A Notification interface with three concrete implementations (Email, SMS, and Push) that can be extended without modifying existing code.
- A working Factory Method pattern with an abstract creator class and concrete creator subclasses that encapsulate object creation.
- A before-and-after comparison showing how the refactoring from naive to factory-based design improves maintainability and follows the Open/Closed Principle.
- Secret Mission: Add a new SlackNotification type to prove the pattern's extensibility without modifying existing code.
Are there any prerequisites?
You should be comfortable with basic Java concepts like classes, interfaces, methods, and running a Java program. No prior knowledge of design patterns is needed.
Write the Naive Approach
Your development environment is set up and ready to go. Now it's time to write some code.
Before learning any design pattern, you need to feel the problem it solves. In this step, you'll write a notification system the "naive" way, with client code that directly creates every object using new. This works, but creates tight coupling that makes the code painful to extend.
In this step, get ready to:
- Create a Notification interface and three concrete implementations.
- Write client code that directly instantiates each notification type.
- Run the program and identify the tight-coupling problem.
Create the Notification interface
Every notification in your system shares one capability: it can send a message. An interface defines this contract so that any notification type must implement a send() method.
- In the VS Code Explorer sidebar, right-click the src folder.
- Select New Folder and name it notification.
- Right-click the new notification folder and select New File.
- Name it Notification.java and paste the following code:
package notification;
public interface Notification {
void send(String message);
}
What does this code do?
The package notification; line tells Java this file belongs to the notification package, which matches the folder name.
The Notification interface declares a single method: send(String message). Any class that implements this interface must provide its own version of send().
- Save Notification.java with Ctrl+S (Windows) or Cmd+S (macOS).
Create the concrete notification classes
Each notification channel (Email, SMS, Push) gets its own class that implements the Notification interface. This is polymorphism in action.
- Right-click the notification folder and select New File.
- Name it EmailNotification.java and paste the following code:
package notification;
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("[Email] Sending: " + message);
}
}
What does this code do?
The implements Notification clause promises that this class provides a send() method. The @Override annotation tells Java (and other developers) that this method fulfills an interface contract.
The send() method prints a tagged message so you can tell which notification type fired when you run the program.
- Save EmailNotification.java.
- Create another new file in the notification folder named SmsNotification.java and paste the following code:
package notification;
public class SmsNotification implements Notification {
@Override
public void send(String message) {
System.out.println("[SMS] Sending: " + message);
}
}
- Save SmsNotification.java.
- Create one more file in the notification folder named PushNotification.java and paste the following code:
package notification;
public class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("[Push] Sending: " + message);
}
}
- Save PushNotification.java.
- Confirm your notification folder now contains four files: Notification.java, EmailNotification.java, SmsNotification.java, and PushNotification.java.
Don't see the notification folder in Explorer?
Make sure you created the folder inside src/, not at the project root. Your folder structure should look like src/notification/. If you accidentally created it in the wrong place, drag it into src using the Explorer sidebar.
help me fix my folder structure for the notification package
Write and run the naive client code
Now for the client code that actually uses these notifications. This is where the problem lives. The client directly creates each concrete object using new, which means it must know about every single notification class.
- Right-click the src folder (not the notification subfolder) and select New File.
- Name it NaiveDemo.java and paste the following code:
import notification.EmailNotification;
import notification.SmsNotification;
import notification.PushNotification;
public class NaiveDemo {
public static void main(String[] args) {
System.out.println("=== Naive Approach (Tightly Coupled) ===\n");
// Problem: Client code directly creates concrete objects
// Adding a new type means changing THIS code
EmailNotification email = new EmailNotification();
SmsNotification sms = new SmsNotification();
PushNotification push = new PushNotification();
email.send("Welcome to our service!");
sms.send("Your verification code is 1234");
push.send("You have a new follower");
System.out.println("\nProblem: This code knows about EVERY concrete class.");
System.out.println("Adding a new type requires modifying this file.");
}
}
What does this code do?
The import lines at the top pull in all three concrete classes. The client must know the exact class name of every notification type it creates.
Inside main(), each notification is created with new and stored as its concrete type (not the interface). This means if you ever add a 4th notification channel, you have to come back to this file and add another import, another new, and another send() call.
- Save NaiveDemo.java.
- Run the program by pressing F5 (or click Run > Start Debugging in the top menu).
- If VS Code asks which file to run, select NaiveDemo.
You should see this output in the terminal panel at the bottom of VS Code:
=== Naive Approach (Tightly Coupled) ===
[Email] Sending: Welcome to our service!
[SMS] Sending: Your verification code is 1234
[Push] Sending: You have a new follower
Problem: This code knows about EVERY concrete class.
Adding a new type requires modifying this file.
Seeing a compilation error?
Check that NaiveDemo.java is directly inside src/ (not inside the notification folder). It has no package declaration, so it must live at the source root.
Also verify that all four files in src/notification/ start with package notification; as the first line. The package declaration must match the folder name exactly.
help me debug my Java compilation error in the notification project
Why is this a problem?
The code works, but it violates the Open/Closed Principle. This principle states that code should be open for extension (adding new features) but closed for modification (you shouldn't have to edit existing code to add something new).
Right now, adding a 4th notification type (like Slack) means modifying NaiveDemo.java. In a real application, that client code might be scattered across dozens of files. Hunting down every new call is error-prone and creates bugs.
✔️ Awesome, I've got everything!
Great. Double check you've saved all five files before moving on.
ⓧ I'd like to double check the full code
Here is the complete code for each file. Compare yours against these:
package notification;
public interface Notification {
void send(String message);
}
package notification;
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("[Email] Sending: " + message);
}
}
package notification;
public class SmsNotification implements Notification {
@Override
public void send(String message) {
System.out.println("[SMS] Sending: " + message);
}
}
package notification;
public class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("[Push] Sending: " + message);
}
}
import notification.EmailNotification;
import notification.SmsNotification;
import notification.PushNotification;
public class NaiveDemo {
public static void main(String[] args) {
System.out.println("=== Naive Approach (Tightly Coupled) ===\n");
// Problem: Client code directly creates concrete objects
// Adding a new type means changing THIS code
EmailNotification email = new EmailNotification();
SmsNotification sms = new SmsNotification();
PushNotification push = new PushNotification();
email.send("Welcome to our service!");
sms.send("Your verification code is 1234");
push.send("You have a new follower");
System.out.println("\nProblem: This code knows about EVERY concrete class.");
System.out.println("Adding a new type requires modifying this file.");
}
}
Your naive notification system works, but it's tightly coupled. In the next step, you'll introduce a Simple Factory to centralize object creation and remove those scattered new calls.
Introduce the Simple Factory
In the previous step, you saw the problem firsthand. Your NaiveDemo.java works, but it's tightly coupled to every concrete notification class.
Now you'll solve that problem with a Simple Factory. A factory is a class whose only job is to create objects on behalf of other code. Instead of scattering new EmailNotification() calls throughout your program, you ask the factory for what you need and it handles the details.
In this step, get ready to:
- Create a NotificationFactory class that centralizes object creation.
- Refactor the client code to use the factory instead of direct instantiation.
- Run the new demo and compare it to the naive approach.
Create the NotificationFactory
The Simple Factory pattern extracts all object creation logic into a single class. The client asks for a notification by type (a string like "EMAIL"), and the factory returns the right implementation.
- In the VS Code Explorer sidebar, right-click the src/notification folder.
- Select New File and name it NotificationFactory.java.
- Add the following code to the file:
package notification;
public class NotificationFactory {
public static Notification createNotification(String type) {
return switch (type.toUpperCase()) {
case "EMAIL" -> new EmailNotification();
case "SMS" -> new SmsNotification();
case "PUSH" -> new PushNotification();
default -> throw new IllegalArgumentException("Unknown notification type: " + type);
};
}
}
What does this code do?
The createNotification method is static, so you can call it without creating a factory instance. It takes a type string and uses Java's enhanced switch expression to return the matching Notification implementation.
The default case throws an IllegalArgumentException if someone passes an unrecognized type. This gives a clear error message instead of a silent failure.
What is a switch expression?
The arrow syntax (->) is Java's enhanced switch expression, available since Java 14. Unlike a traditional switch statement, it returns a value directly and doesn't need break statements. This makes factory code much cleaner.
- Save NotificationFactory.java.
Refactor the client to use the factory
Now you'll write a new demo that uses the factory instead of directly calling new on concrete classes. The client will only know about the Notification interface and the NotificationFactory.
- In the Explorer sidebar, right-click the src folder (not the notification subfolder).
- Select New File and name it SimpleFactoryDemo.java.
- Add the following code to the file:
import notification.Notification;
import notification.NotificationFactory;
public class SimpleFactoryDemo {
public static void main(String[] args) {
System.out.println("=== Simple Factory Approach ===\n");
// Better: Client only knows the interface and factory
Notification email = NotificationFactory.createNotification("EMAIL");
Notification sms = NotificationFactory.createNotification("SMS");
Notification push = NotificationFactory.createNotification("PUSH");
email.send("Welcome to our service!");
sms.send("Your verification code is 1234");
push.send("You have a new follower");
System.out.println("\nImprovement: Client doesn't import concrete classes.");
System.out.println("Limitation: Adding a new type still requires modifying the factory switch.");
}
}
What does this code do?
Look at the imports at the top. The client only imports Notification (the interface) and NotificationFactory. It has zero knowledge of EmailNotification, SmsNotification, or PushNotification.
Each variable is declared as the Notification interface type, not a concrete class. This is programming to an interface. The factory decides which concrete class to instantiate behind the scenes.
- Save SimpleFactoryDemo.java.
Seeing red underlines in VS Code?
If the Java Language Server shows errors on your import lines, make sure NotificationFactory.java is saved and lives inside the src/notification/ folder. The Language Server sometimes takes a moment to reindex new files.
help me fix Java import errors in VS Code
Run and compare both approaches
- Open SimpleFactoryDemo.java in the editor.
- Press F5 to run the program.
You should see this output in the terminal:
=== Simple Factory Approach ===
[Email] Sending: Welcome to our service!
[SMS] Sending: Your verification code is 1234
[Push] Sending: You have a new follower
Improvement: Client doesn't import concrete classes.
Limitation: Adding a new type still requires modifying the factory switch.
How does this compare to the naive approach?
The notification output is identical. Both approaches send the same messages through the same channels. The difference is entirely structural.
In NaiveDemo.java, the client imports and instantiates three concrete classes directly. In SimpleFactoryDemo.java, the client only knows the Notification interface. The factory handles the rest.
This is loose coupling in action. The client can work with any notification type without being rewritten.
What is the remaining limitation?
The Simple Factory still has one weakness. If you want to add a new notification type (like Slack), you'd have to open NotificationFactory.java and add a new case to the switch expression. This violates the Open/Closed Principle. Classes should be open for extension but closed for modification.
The next step solves this with the full Factory Method pattern.
Program won't run?
Make sure SimpleFactoryDemo.java is the active file in the editor when you press F5. VS Code runs whichever file contains the cursor.
Also check that both NotificationFactory.java and SimpleFactoryDemo.java are saved. Unsaved files show a dot on their tab.
help me debug why my Java program won't run in VS Code
✔️ Awesome, I've got everything!
Great. Double check that both files are saved.
ⓧ I'd like to double check the full code
Here's NotificationFactory.java:
package notification;
public class NotificationFactory {
public static Notification createNotification(String type) {
return switch (type.toUpperCase()) {
case "EMAIL" -> new EmailNotification();
case "SMS" -> new SmsNotification();
case "PUSH" -> new PushNotification();
default -> throw new IllegalArgumentException("Unknown notification type: " + type);
};
}
}
Here's SimpleFactoryDemo.java:
import notification.Notification;
import notification.NotificationFactory;
public class SimpleFactoryDemo {
public static void main(String[] args) {
System.out.println("=== Simple Factory Approach ===\n");
// Better: Client only knows the interface and factory
Notification email = NotificationFactory.createNotification("EMAIL");
Notification sms = NotificationFactory.createNotification("SMS");
Notification push = NotificationFactory.createNotification("PUSH");
email.send("Welcome to our service!");
sms.send("Your verification code is 1234");
push.send("You have a new follower");
System.out.println("\nImprovement: Client doesn't import concrete classes.");
System.out.println("Limitation: Adding a new type still requires modifying the factory switch.");
}
}
You've centralized object creation into a single factory class. The client code is cleaner and decoupled from concrete types. Next up, you'll take this a step further with the full Factory Method pattern, which eliminates even the need to modify a switch statement when adding new types.
Implement the Full Factory Method Pattern
The Simple Factory you built in the previous step centralizes object creation, which is a big improvement. But it still has a limitation: adding a new notification type means modifying the factory's switch statement.
The Factory Method pattern eliminates this problem entirely. Instead of one factory with a switch, each notification type gets its own dedicated creator class. Adding a new channel means creating a new file, not editing existing code.
In this step, get ready to:
- Create an abstract creator class with a factory method and business logic.
- Build three concrete creator subclasses that each produce a specific notification type.
- Write a demo client that uses creators without knowing concrete notification classes.
Create the abstract creator class
The abstract creator is the heart of the Factory Method pattern. It defines an abstract method that subclasses must implement to produce a product, and it also contains business logic that uses that product without knowing its concrete type.
- Inside the src/notification/ folder, create a new file called NotificationCreator.java and add the following code:
package notification;
public abstract class NotificationCreator {
// Factory Method - subclasses decide what to create
protected abstract Notification createNotification();
// Business logic that uses the factory method
public void notifyUser(String message) {
Notification notification = createNotification();
System.out.println("Preparing to send notification...");
notification.send(message);
System.out.println("Notification sent successfully!\n");
}
}
What does this code do?
This abstract class has two methods that work together:
- createNotification() is the factory method. It's abstract, so each subclass must provide its own implementation that returns a specific notification type.
- notifyUser(String message) is the business logic. It calls the factory method to get a notification, then uses it. This method never needs to know which concrete type it received.
This separation is the key insight of the pattern: business logic lives in the parent class, while creation decisions live in each subclass.
- Save NotificationCreator.java.
Seeing a red underline on the class?
- Make sure the file is inside the src/notification/ folder, not directly in src/. The package notification; declaration must match the folder path.
- Check that Notification.java exists in the same folder. The abstract class references the Notification interface.
Why is my NotificationCreator showing errors?
✔️ Awesome, I've got everything!
Great. Make sure you've saved the file before moving on.
ⓧ I'd like to double check the full code
package notification;
public abstract class NotificationCreator {
// Factory Method - subclasses decide what to create
protected abstract Notification createNotification();
// Business logic that uses the factory method
public void notifyUser(String message) {
Notification notification = createNotification();
System.out.println("Preparing to send notification...");
notification.send(message);
System.out.println("Notification sent successfully!\n");
}
}
Create the concrete creator classes
Each concrete creator is responsible for one notification type. It extends NotificationCreator and overrides the factory method to return its specific product. This is polymorphism in action.
- Inside src/notification/, create a new file called EmailNotificationCreator.java and add the following code:
package notification;
public class EmailNotificationCreator extends NotificationCreator {
@Override
protected Notification createNotification() {
return new EmailNotification();
}
}
What does this code do?
This class has one job: return a new EmailNotification when the factory method is called. The parent class's notifyUser() method handles all the business logic. The creator just decides which product to build.
The next two creators follow the exact same pattern, just returning a different notification type.
- Create another file called SmsNotificationCreator.java in the same src/notification/ folder and add this code:
package notification;
public class SmsNotificationCreator extends NotificationCreator {
@Override
protected Notification createNotification() {
return new SmsNotification();
}
}
- Create one more file called PushNotificationCreator.java in the same folder and add this code:
package notification;
public class PushNotificationCreator extends NotificationCreator {
@Override
protected Notification createNotification() {
return new PushNotification();
}
}
- Save all three creator files.
- In the VS Code Explorer sidebar, expand the src/notification/ folder. You should see EmailNotificationCreator.java, SmsNotificationCreator.java, and PushNotificationCreator.java alongside your existing files.
Seeing errors in the creator files?
- Verify that NotificationCreator.java is saved. The concrete creators extend that class, so it must exist first.
- Check that each file has package notification; as its first line. All files in the notification/ folder need this declaration.
- Make sure the class names match the file names exactly. Java requires EmailNotificationCreator to live in EmailNotificationCreator.java.
Why are my creator classes showing compile errors?
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before continuing.
ⓧ I'd like to double check the full code
package notification;
public class EmailNotificationCreator extends NotificationCreator {
@Override
protected Notification createNotification() {
return new EmailNotification();
}
}
package notification;
public class SmsNotificationCreator extends NotificationCreator {
@Override
protected Notification createNotification() {
return new SmsNotification();
}
}
package notification;
public class PushNotificationCreator extends NotificationCreator {
@Override
protected Notification createNotification() {
return new PushNotification();
}
}
Why this approach instead of the Simple Factory?
The Simple Factory uses a single switch statement to decide which object to create. Every new type requires editing that switch. The Factory Method pattern distributes this responsibility: each creator class owns the creation logic for exactly one product type.
This follows the Open/Closed Principle. Your code is open for extension (add new creators) but closed for modification (no existing class needs to change). In professional codebases where multiple developers add features simultaneously, this prevents merge conflicts and regression bugs.
Write the Factory Method demo
Now you need a client class that uses the creators. This demo shows how the client works exclusively with the abstract NotificationCreator type. It never imports or references a concrete notification class.
- In the src/ folder (not inside notification/), create a new file called FactoryMethodDemo.java and add the following code:
import notification.NotificationCreator;
import notification.EmailNotificationCreator;
import notification.SmsNotificationCreator;
import notification.PushNotificationCreator;
public class FactoryMethodDemo {
public static void main(String[] args) {
System.out.println("=== Factory Method Pattern ===\n");
// Best: Each creator encapsulates its own creation logic
// Adding a new type = creating a new Creator class (no existing code changes)
NotificationCreator[] creators = {
new EmailNotificationCreator(),
new SmsNotificationCreator(),
new PushNotificationCreator()
};
String[] messages = {
"Welcome to our service!",
"Your verification code is 1234",
"You have a new follower"
};
for (int i = 0; i < creators.length; i++) {
creators[i].notifyUser(messages[i]);
}
System.out.println("Advantage: To add a new notification type,");
System.out.println("create a new Notification + Creator class.");
System.out.println("No existing code is modified (Open/Closed Principle).");
}
}
What does this code do?
- The creators array holds three creator objects, but its type is NotificationCreator[]. The client only knows the abstract parent type.
- The for loop calls notifyUser() on each creator. At runtime, Java's polymorphic dispatch calls the correct createNotification() override in each concrete creator.
- Notice the imports: the client imports the creator classes, not EmailNotification, SmsNotification, or PushNotification. The concrete products are fully encapsulated behind their creators.
- Save FactoryMethodDemo.java.
- Check that VS Code shows no red underlines in the file. If the Java Language Server has loaded, you should see the imports resolve without errors.
Seeing unresolved import errors?
- Make sure all four creator files (NotificationCreator.java plus the three concrete ones) are saved inside src/notification/.
- Verify that FactoryMethodDemo.java is directly inside src/, not inside the notification/ subfolder. It has no package declaration, so it must be at the top level of src/.
- If errors persist, try closing and reopening VS Code. The Java Language Server occasionally needs a restart to pick up new files.
Help me fix my import errors in FactoryMethodDemo.java.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
import notification.NotificationCreator;
import notification.EmailNotificationCreator;
import notification.SmsNotificationCreator;
import notification.PushNotificationCreator;
public class FactoryMethodDemo {
public static void main(String[] args) {
System.out.println("=== Factory Method Pattern ===\n");
// Best: Each creator encapsulates its own creation logic
// Adding a new type = creating a new Creator class (no existing code changes)
NotificationCreator[] creators = {
new EmailNotificationCreator(),
new SmsNotificationCreator(),
new PushNotificationCreator()
};
String[] messages = {
"Welcome to our service!",
"Your verification code is 1234",
"You have a new follower"
};
for (int i = 0; i < creators.length; i++) {
creators[i].notifyUser(messages[i]);
}
System.out.println("Advantage: To add a new notification type,");
System.out.println("create a new Notification + Creator class.");
System.out.println("No existing code is modified (Open/Closed Principle).");
}
}
Your Factory Method architecture is complete. You now have an abstract creator, three concrete creators, and a client that uses them. Next up, you'll run all three demo files and compare how each approach handles adding a new notification type.
Run, Debug, and Compare All Three Approaches
You've built three versions of the same notification system, each with a different level of decoupling. Now it's time to see the final version in action and use the debugger to watch polymorphism happen at runtime.
Running code tells you it works. Debugging tells you how it works. By stepping through the Factory Method demo, you'll see the abstract notifyUser() method dispatch to a different concrete creator on each iteration.
In this step, get ready to:
- Run the Factory Method demo and observe the output.
- Debug with a breakpoint to watch polymorphic dispatch in real time.
- Compare all three approaches to understand the extensibility gains.
Run the Factory Method demo
The Factory Method demo is the culmination of your refactoring journey. It uses abstract creators to encapsulate object creation, so the client loop never names a concrete notification class.
- In the VS Code Explorer sidebar, open src/FactoryMethodDemo.java.
- Press F5 to start debugging. VS Code auto-detects the main() method and launches the program.
In the DEBUG CONSOLE panel at the bottom of VS Code, you should see output like this:
=== Factory Method Pattern ===
Preparing to send notification...
[Email] Sending: Welcome to our service!
Notification sent successfully!
Preparing to send notification...
[SMS] Sending: Your verification code is 1234
Notification sent successfully!
Preparing to send notification...
[Push] Sending: You have a new follower
Notification sent successfully!
Advantage: To add a new notification type,
create a new Notification + Creator class.
No existing code is modified (Open/Closed Principle).
What does this output show?
Each creator's notifyUser() method prints "Preparing to send notification..." before delegating to the concrete notification. The loop in main() doesn't know whether it's dealing with an email, SMS, or push notification.
The client code treats every creator identically. The concrete type is decided inside each creator subclass, not at the call site.
Not seeing any output?
- Make sure FactoryMethodDemo.java is the active file in the editor when you press F5. VS Code runs the file currently open.
- Check the DEBUG CONSOLE tab (not the Terminal tab) for output. The debugger prints there by default.
- If you see a compilation error, check that all files in src/notification/ have the correct package notification; declaration at the top.
Why isn't my FactoryMethodDemo running in VS Code?
Debug with a breakpoint inside notifyUser()
Running confirms the code works. Debugging lets you watch the polymorphic dispatch happen line by line, seeing which concrete notification object gets created by each creator.
- Open src/notification/NotificationCreator.java in the editor.
- Find the line Notification notification = createNotification(); inside the notifyUser() method.
- Click in the gutter (the space to the left of the line number) to set a breakpoint. A red dot appears.
- Switch back to FactoryMethodDemo.java and press F5 to start debugging again.
Execution pauses at your breakpoint. The VARIABLES panel on the left shows the current state.
- Press F11 (Step Into) to step into the createNotification() call. VS Code jumps into the concrete creator (e.g. EmailNotificationCreator).
- Press F5 (Continue) to resume execution until the breakpoint is hit again on the next loop iteration.
- Press F11 again. This time VS Code jumps into SmsNotificationCreator instead.
- Press F5 once more, then F11 to see it jump into PushNotificationCreator.
- Press F5 to let the program finish.
What just happened?
The same line of code (createNotification()) dispatched to three different classes across three loop iterations. This is polymorphism in action.
The abstract NotificationCreator never knows which notification it creates. Each concrete subclass makes that decision. This is exactly what the Factory Method pattern is designed to do.
Breakpoint not being hit?
- Make sure your breakpoint is on the Notification notification = createNotification(); line inside NotificationCreator.java, not on a comment or blank line.
- Confirm you launched FactoryMethodDemo.java (not one of the other demo files). Only this demo calls notifyUser().
Help me debug why my breakpoint isn't working in VS Code Java.
Compare the three approaches side by side
Now that you've seen the Factory Method in action, compare it against the two earlier approaches to understand the progression from tightly-coupled to fully decoupled code.
- Open NaiveDemo.java, SimpleFactoryDemo.java, and FactoryMethodDemo.java in separate editor tabs.
- Look at the import statements at the top of each file. Notice how knowledge of concrete classes shrinks with each approach.
How do the imports tell the story?
- NaiveDemo.java imports EmailNotification, SmsNotification, and PushNotification. The client knows every single concrete type.
- SimpleFactoryDemo.java imports only Notification and NotificationFactory. The concrete classes are hidden behind the factory.
- FactoryMethodDemo.java imports the abstract NotificationCreator and concrete creators. The notification products themselves are completely invisible to the client.
- Think about what happens when you need to add a "SlackNotification" to each approach:
- Naive approach: You'd add a new new SlackNotification() call to the client code and a new import. Every file that creates notifications must change.
- Simple Factory: You'd modify the switch statement inside NotificationFactory.java. Better, but you're still modifying existing code.
- Factory Method: You'd create two new files (SlackNotification.java and SlackNotificationCreator.java) and nothing else changes. Existing code is untouched.
What is the Open/Closed Principle?
The Open/Closed Principle states that classes should be open for extension but closed for modification. You should be able to add new behavior without editing existing code.
The Factory Method pattern satisfies this perfectly. Adding a new notification channel means creating new files. Zero existing classes need to change.
Secret mission
Add SlackNotification Without Changing Existing Code
You've built a Factory Method architecture that promises easy extensibility. Now it's time to prove it. Add an entirely new notification channel without touching a single existing class.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The factory-pattern-demo project folder on your Desktop.
- JDK 21 (Microsoft Build of OpenJDK) installed via winget.
- Extension Pack for Java installed in VS Code.
Keep everything running
No action needed. Choose this if you're still actively building or want to experiment with more design patterns in this project.
- Your factory-pattern-demo folder stays on your Desktop.
- JDK 21 and the Extension Pack for Java remain installed for future Java projects.
Pause - I'll come back to this later
Close VS Code to free up memory. Your project files stay on disk and are ready to reopen anytime.
- Close VS Code.
- Your project folder, JDK, and extensions remain installed. Reopen the folder in VS Code when you're ready to continue.
Delete - I don't want to use this again
Remove all project resources and tools. Follow these instructions to clean up completely.
- Delete the project folder. On macOS/Linux, run:
rm -rf ~/Desktop/factory-pattern-demo
On Windows, run:
rmdir /s /q "$HOME\Desktop\factory-pattern-demo"
- Uninstall JDK 21 by running this in PowerShell or Command Prompt:
winget uninstall Microsoft.OpenJDK.21
- Uninstall the Extension Pack for Java in VS Code. Open the Extensions view (Ctrl+Shift+X), find Extension Pack for Java, and click Uninstall.
Nice Work!
Nice Work!
You've just built a notification system that evolves from tightly-coupled code to a professional Factory Method architecture in Java. You now have a reusable mental model for recognizing when scattered new calls signal the need for a factory.
You've learned how to:
- Define a Notification interface and three concrete implementations (Email, SMS, Push), progressing from a naive approach to a Simple Factory to the full Factory Method pattern with abstract creators and polymorphic dispatch.
- Identify when code needs a factory by spotting Open/Closed Principle violations and scattered object creation logic that makes adding new types painful.
- Debug polymorphic code in VS Code's Java debugger, stepping through abstract creator methods to see how the factory method dispatches to concrete subclasses at runtime.
- Added a SlackNotification channel without modifying any existing classes, proving the Factory Method pattern's extensibility in action.
Ready to quiz yourself?