Java Design Patterns in Practice

Build a multi-channel notification service using 5 core Java design patterns.

Introduction

30 Second Summary

Every app you use sends notifications through multiple channels. Behind the scenes, the code handling all these channels can quickly become a tangled mess of if/else statements.

In this project, you will build a multi-channel Notification Service in pure Java that dispatches messages via Email, SMS, and Push. You'll learn five essential design patterns by starting with messy code and progressively refactoring it into clean, extensible architecture.

What You'll Build

A pure Java application that routes notifications through Email, SMS, and Push channels using five production-level design patterns working together.

By the end of this project, you'll have:

  • A working Notification Service that builds, routes, decorates, and broadcasts messages through multiple channels.
  • Hands-on experience with the Builder, Strategy, Factory, Decorator, and Observer patterns.
  • A mental model for when to reach for each pattern in production Java systems.
  • Secret Mission: A priority-based notification queue that processes urgent messages first.

Are there any prerequisites?

You should be comfortable with basic Java syntax including classes, interfaces, methods, and inheritance. No framework knowledge is required.

Before We Start

Before We Start...

Set Up Your Java Development Environment

Before you can build the Notification Service, you need two things on your machine: a Java Development Kit (JDK) to compile and run Java code, and an IDE to write it in.

In this step, you will install JDK 21 and IntelliJ IDEA. Then you will create the project structure that holds all the pattern code you will write throughout this project.

In this step, get ready to:
  • Install JDK 21 and verify it works in your terminal.
  • Install IntelliJ IDEA and complete the setup wizard.
  • Create the notification-service project with the correct package structure.
Install JDK 21

The JDK includes the Java compiler and runtime. Your project needs JDK 21 specifically because it includes modern features like switch expressions that the Factory pattern code relies on.

  • Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check if Java is already installed by running this command:
java -version

✔️ I see version 21 or higher

Your terminal shows a version number starting with 21. JDK 21 is ready to go.

  • Also confirm the compiler is available by running:
javac -version

You should see a version starting with 21. You are all set to move on.

ⓧ I see an older version

Your system has an older Java version installed. You need JDK 21 for the switch expressions used in this project. Download and install JDK 21 to replace it.

  • Open the Oracle JDK 21 downloads page in your browser:

https://www.oracle.com/java/technologies/downloads/

  • Select the Java 21 tab at the top of the page.
  • Download the installer for your operating system (Windows x64 Installer for Windows, macOS Installer for Mac).
  • Run the installer. On Windows, administrator privileges are required.
  • Follow the prompts and accept the defaults.
  • Close your terminal completely and reopen it so the new version is picked up.
  • Verify the installation by running:
java -version

You should now see a version starting with 21. Also confirm the compiler:

javac -version

macOS still showing old version?

macOS may have a system Java that shadows new installs. Run /usr/libexec/java_home -V to see all installed JDKs. If JDK 21 appears but is not the default, set JAVA_HOME to the JDK 21 path shown in that output.

ⓧ Command not found

Java is not installed on your machine yet. Download and install JDK 21 from Oracle.

  • Open the Oracle JDK 21 downloads page in your browser:

https://www.oracle.com/java/technologies/downloads/

  • Select the Java 21 tab at the top of the page.
  • Download the installer for your operating system (Windows x64 Installer for Windows, macOS Installer for Mac).
  • Run the installer. On Windows, administrator privileges are required.
  • Follow the prompts and accept the defaults.
  • Close your terminal completely and reopen it.
  • Verify the installation by running:
java -version

You should see a version starting with 21. Also confirm the compiler:

javac -version

Still seeing command not found?

The JDK installer may not have added Java to your system PATH. On Windows, open Environment Variables in System Settings and add the JDK's bin folder (inside Program Files/Java/jdk-21) to your PATH variable. On macOS, the installer handles this automatically.

help me fix Java not being found in my terminal after installing JDK 21

Why JDK 21?

JDK 21 is a Long-Term Support (LTS) release. It is free under Oracle's No-Fee Terms and Conditions until September 2026.

This project uses Java switch expressions (finalized in Java 14) in the Factory pattern. JDK 21 includes this feature and is the most widely-used LTS in production environments today.

Install IntelliJ IDEA

IntelliJ IDEA is the standard IDE for professional Java development. It provides code completion, refactoring tools, and a built-in project builder that compiles and runs your code.

  • Open the IntelliJ IDEA download page in your browser:

https://www.jetbrains.com/idea/download/

  • Click the Download button for your operating system.
  • Run the downloaded installer.
  • Follow the installation prompts and accept the defaults.
  • Once installation completes, launch IntelliJ IDEA. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type IntelliJ IDEA and press Enter to open it.
  • Complete the initial setup wizard. Accept the default theme and plugin selections.

Is IntelliJ IDEA free?

Since version 2025.3, IntelliJ IDEA ships as a single unified product. Core Java development features (editing, compiling, debugging, refactoring) are free without a subscription or activation. You do not need a paid license for this project.

Having trouble installing?

Make sure your machine meets the minimum requirements: a 64-bit CPU with 4 cores, 8 GB RAM, and 10 GB of free disk space.

help me troubleshoot my IntelliJ IDEA installation

Create the notification-service project

Now that your tools are installed, you will create the Java project that holds all the code for this tutorial.

  • In IntelliJ IDEA, you should see the Welcome screen. Click New Project.
  • In the New Project wizard, select Java from the list on the left.
  • In the Name field, type notification-service.
  • Make sure IntelliJ is selected as the Build system.
  • In the JDK dropdown, select the JDK 21 you just installed.
  • Uncheck Add sample code if it is enabled.
  • Click Create.

Don't see JDK 21 in the dropdown?

If JDK 21 does not appear in the JDK list, select Add JDK from Disk... and navigate to where JDK 21 was installed. On Windows, this is typically Program Files/Java/jdk-21. On macOS, check /Library/Java/JavaVirtualMachines/.

Alternatively, select Download JDK and choose version 21 from the list. IntelliJ will download and configure it for you.

IntelliJ IDEA creates the project and opens the main editor window. You should see the Project tool window on the left showing a src folder.

Next, create the package that will hold all your Java source files.

  • In the Project tool window on the left, right-click the src folder.
  • Select New, then Package.
  • Type com.patterns.notifications and press Enter.

You should see the nested folder structure com/patterns/notifications appear under src in the Project tool window.

What is a Java package?

A package groups related classes together and prevents naming conflicts. The convention is to use a reversed domain name (like com.patterns.notifications) to keep packages unique across projects. Every Java file you create in this project will live inside this package.

Don't see the src folder?

Make sure the Project tool window is open. Press Alt+1 (Windows) or Cmd+1 (macOS) to toggle it. The src folder should be visible at the top level of your project.

help me find the src folder in my IntelliJ IDEA project

Your development environment is ready. Next up, you will write the first version of the Notification Service to see exactly what problems design patterns solve.

Build the Naive Implementation

Your project structure is ready. Now it is time to write some actual Java code.

In this step, you will build a notification service the quick and dirty way. The code will work, but it will be painful to maintain. Experiencing that pain firsthand is the best motivation for learning design patterns.

In this step, get ready to:
  • Create a Notification class with all required fields.
  • Build a NaiveNotificationService that uses if/else chains to handle multiple channels.
  • Identify the design problems that motivate pattern-based refactoring.
Create the Notification class

Every notification needs a recipient, a message, and a channel. Some channels also need a subject line or a priority level. For now, you will use a single constructor that takes all of these fields at once.

  • In IntelliJ IDEA, expand the src folder in the Project tool window on the left.
  • Right-click on the com.patterns.notifications package.
  • Select New then Java Class.
  • Type Notification as the class name and press Enter.
  • Replace the generated code with the following:
package com.patterns.notifications;

public class Notification {
    // Core fields every notification needs
    private String recipient;
    private String message;
    private String channel;

    // Optional fields - awkward when not needed
    private String subject;
    private int priority;

    // All-args constructor forces callers to provide every field
    public Notification(String recipient, String message, String channel, String subject, int priority) {
        this.recipient = recipient;
        this.message = message;
        this.channel = channel;
        this.subject = subject;
        this.priority = priority;
    }

    public String getRecipient() { return recipient; }
    public String getMessage() { return message; }
    public String getChannel() { return channel; }
    public String getSubject() { return subject; }
    public int getPriority() { return priority; }
}

What does this code do?

This class holds the data for one notification. The five fields cover the basics: who gets it, what it says, which channel to use, an optional subject line, and a priority number.

The all-args constructor forces every caller to pass all five values, even when some are irrelevant. An SMS does not need a subject, but you still have to pass null. This awkwardness is intentional for now.

  • Save Notification.java by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Check that no red underlines appear in the editor. A clean file means no syntax errors.

Seeing red underlines?

Make sure the first line reads package com.patterns.notifications; and matches your folder structure exactly. If the package declaration does not match the folder path, IntelliJ shows an error.

Help me fix my Notification.java errors.

Create the NaiveNotificationService and Main classes

Now you need a service class that decides how to deliver each notification. The naive approach is to use if/else chains that check the channel string and run different formatting logic for each.

  • Right-click on the com.patterns.notifications package again.
  • Select New then Java Class.
  • Name it NaiveNotificationService and press Enter.
  • Replace the generated code with the following:
package com.patterns.notifications;

public class NaiveNotificationService {

    // One giant method that handles all channels with if/else
    public void send(Notification notification) {
        if (notification.getChannel().equals("EMAIL")) {
            // Email-specific formatting logic
            String subject = notification.getSubject() != null ? notification.getSubject() : "No Subject";
            System.out.println("[EMAIL] Sending to " + notification.getRecipient());
            System.out.println("Subject: " + subject);
            System.out.println("Body: " + notification.getMessage());
            System.out.println("Status: Delivered via SMTP");

        } else if (notification.getChannel().equals("SMS")) {
            // SMS-specific formatting logic
            String truncated = notification.getMessage().length() > 160
                    ? notification.getMessage().substring(0, 157) + "..."
                    : notification.getMessage();
            System.out.println("[SMS] Sending to " + notification.getRecipient());
            System.out.println("Message: " + truncated);
            System.out.println("Status: Delivered via SMS Gateway");

        } else if (notification.getChannel().equals("PUSH")) {
            // Push-specific formatting logic
            System.out.println("[PUSH] Sending to device token: " + notification.getRecipient());
            System.out.println("Alert: " + notification.getMessage());
            System.out.println("Status: Delivered via Push Service");

        } else {
            System.out.println("Unknown channel: " + notification.getChannel());
        }
    }
}

What does this code do?

The send() method checks the channel string and runs completely different logic for each one. Email needs a subject line fallback. SMS truncates messages to 160 characters. Push uses a device token.

All of this formatting logic lives inside a single method. Imagine adding a Slack channel, a WhatsApp channel, and a webhook channel. Each new channel means adding another else if branch to this growing method.

  • Save NaiveNotificationService.java.

Now create the entry point that runs everything.

  • Right-click on the com.patterns.notifications package.
  • Select New then Java Class.
  • Name it Main and press Enter.
  • Replace the generated code with the following:
package com.patterns.notifications;

public class Main {

    public static void main(String[] args) {
        System.out.println("=== NAIVE NOTIFICATION SERVICE ===\n");

        NaiveNotificationService service = new NaiveNotificationService();

        // Email notification - subject is useful here
        Notification email = new Notification(
                "alice@company.com", "Your order has shipped!", "EMAIL", "Order Update", 2);
        service.send(email);

        System.out.println();

        // SMS notification - subject is null (awkward!)
        Notification sms = new Notification(
                "+1-555-0123", "Your verification code is 482910", "SMS", null, 1);
        service.send(sms);

        System.out.println();

        // Push notification - subject is null again
        Notification push = new Notification(
                "device-token-abc123", "You have a new message from Bob", "PUSH", null, 3);
        service.send(push);
    }
}

What does this code do?

The main() method creates three notifications (one for each channel) and sends them through the naive service. Notice how the SMS and Push constructors pass null for the subject because those channels do not use it.

This is the readability problem. When you see null, 1 at the end of a constructor call, you have no idea what those values mean without checking the class definition.

  • Save Main.java.
  • Right-click anywhere inside the Main.java editor and select Run 'Main.main()'.
  • Check the Run panel at the bottom of IntelliJ. You should see output for all three channels with their formatted messages.

Not seeing the Run option?

Make sure your Main.java file has a public static void main(String[] args) method. IntelliJ only shows the run option when it detects a valid entry point. Also verify the file is inside the correct package folder.

Help me run my Main.java in IntelliJ IDEA.

Identify what is wrong with this approach

The code works. All three notifications print to the console. But look at what happens when the project grows.

Why is this code a problem?

This naive implementation violates the Open/Closed Principle. Every new channel (Slack, WhatsApp, webhook) forces you to open NaiveNotificationService.java and add another else if branch. A class should be open for extension but closed for modification.

The class also mixes two responsibilities: deciding which channel to use and formatting the message for that channel. If you need to change email formatting, you are editing the same file that handles SMS truncation logic.

Finally, there is no way to add cross-cutting behavior like logging or retry without editing the existing code. You would have to paste logging statements into every single branch.

These three problems are exactly what design patterns solve. In the next steps, you will refactor this code one pattern at a time.

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files before moving on.

ⓧ I'd like to double check the full code

Here is the full code for each file at this point.

package com.patterns.notifications;

public class Notification {
    // Core fields every notification needs
    private String recipient;
    private String message;
    private String channel;

    // Optional fields - awkward when not needed
    private String subject;
    private int priority;

    // All-args constructor forces callers to provide every field
    public Notification(String recipient, String message, String channel, String subject, int priority) {
        this.recipient = recipient;
        this.message = message;
        this.channel = channel;
        this.subject = subject;
        this.priority = priority;
    }

    public String getRecipient() { return recipient; }
    public String getMessage() { return message; }
    public String getChannel() { return channel; }
    public String getSubject() { return subject; }
    public int getPriority() { return priority; }
}
package com.patterns.notifications;

public class NaiveNotificationService {

    // One giant method that handles all channels with if/else
    public void send(Notification notification) {
        if (notification.getChannel().equals("EMAIL")) {
            // Email-specific formatting logic
            String subject = notification.getSubject() != null ? notification.getSubject() : "No Subject";
            System.out.println("[EMAIL] Sending to " + notification.getRecipient());
            System.out.println("Subject: " + subject);
            System.out.println("Body: " + notification.getMessage());
            System.out.println("Status: Delivered via SMTP");

        } else if (notification.getChannel().equals("SMS")) {
            // SMS-specific formatting logic
            String truncated = notification.getMessage().length() > 160
                    ? notification.getMessage().substring(0, 157) + "..."
                    : notification.getMessage();
            System.out.println("[SMS] Sending to " + notification.getRecipient());
            System.out.println("Message: " + truncated);
            System.out.println("Status: Delivered via SMS Gateway");

        } else if (notification.getChannel().equals("PUSH")) {
            // Push-specific formatting logic
            System.out.println("[PUSH] Sending to device token: " + notification.getRecipient());
            System.out.println("Alert: " + notification.getMessage());
            System.out.println("Status: Delivered via Push Service");

        } else {
            System.out.println("Unknown channel: " + notification.getChannel());
        }
    }
}
package com.patterns.notifications;

public class Main {

    public static void main(String[] args) {
        System.out.println("=== NAIVE NOTIFICATION SERVICE ===\n");

        NaiveNotificationService service = new NaiveNotificationService();

        // Email notification - subject is useful here
        Notification email = new Notification(
                "alice@company.com", "Your order has shipped!", "EMAIL", "Order Update", 2);
        service.send(email);

        System.out.println();

        // SMS notification - subject is null (awkward!)
        Notification sms = new Notification(
                "+1-555-0123", "Your verification code is 482910", "SMS", null, 1);
        service.send(sms);

        System.out.println();

        // Push notification - subject is null again
        Notification push = new Notification(
                "device-token-abc123", "You have a new message from Bob", "PUSH", null, 3);
        service.send(push);
    }
}

Your naive service is running. In the next step, you will tackle the first problem (the awkward constructor) by introducing the Builder Pattern.

Apply the Builder Pattern

In the previous step, you saw how the all-args constructor forces you to remember parameter order and pass null for optional fields. This becomes painful the moment you have more than three or four parameters.

The Builder Pattern solves this by letting you construct objects step-by-step using readable method calls. In this step, you will refactor Notification.java to use a fluent Builder API.

In this step, get ready to:
  • Replace the telescoping constructor with a static inner Builder class.
  • Update Main.java to construct notifications using the fluent Builder API.
  • Run the project to verify the Builder produces valid Notification objects.
Refactor the Notification class with a Builder

A Builder separates the construction of an object from its representation. Instead of one massive constructor call, you chain named methods that clearly describe each field being set.

  • Open Notification.java in IntelliJ IDEA.
  • Delete the entire contents of the file and replace it with the following code:
package com.patterns.notifications;

public class Notification {
    private final String recipient;
    private final String message;
    private final String channel;
    private final String subject;
    private final int priority;

    private Notification(Builder builder) {
        this.recipient = builder.recipient;
        this.message = builder.message;
        this.channel = builder.channel;
        this.subject = builder.subject;
        this.priority = builder.priority;
    }

    public String getRecipient() { return recipient; }
    public String getMessage() { return message; }
    public String getChannel() { return channel; }
    public String getSubject() { return subject; }
    public int getPriority() { return priority; }

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

    @Override
    public String toString() {
        return String.format("Notification{to='%s', channel='%s', priority=%d, message='%s'}",
                recipient, channel, priority, message);
    }

    public static class Builder {
        private String recipient;
        private String message;
        private String channel;
        private String subject;
        private int priority = 5;

        public Builder to(String recipient) {
            this.recipient = recipient;
            return this;
        }

        public Builder withMessage(String message) {
            this.message = message;
            return this;
        }

        public Builder viaChannel(String channel) {
            this.channel = channel;
            return this;
        }

        public Builder withSubject(String subject) {
            this.subject = subject;
            return this;
        }

        public Builder withPriority(int priority) {
            this.priority = priority;
            return this;
        }

        public Notification build() {
            if (recipient == null || message == null || channel == null) {
                throw new IllegalStateException("recipient, message, and channel are required");
            }
            return new Notification(this);
        }
    }
}

What does this code do?

The Notification constructor is now private. The only way to create a Notification is through the Builder.

  • All fields are final, making the object immutable once built.
  • Each fluent method (to(), withMessage(), etc.) returns this so you can chain calls together.
  • The build() method validates that required fields are present before creating the object.
  • The priority field defaults to 5, so you only set it when you need a different value.
  • Save Notification.java.

Seeing red underlines in IntelliJ?

Your Main.java still references the old all-args constructor, so compilation errors are expected. You will fix this in the next substep.

If you see errors inside Notification.java itself, make sure the package declaration at the top matches com.patterns.notifications exactly.

Help me debug errors in Notification.java after adding the Builder pattern

Update Main.java to use the Builder

Now that the old constructor is gone, you need to update Main.java to construct notifications using the new fluent API.

  • Open Main.java and replace its entire contents with the following code:
package com.patterns.notifications;

public class Main {

    public static void main(String[] args) {
        System.out.println("=== BUILDER PATTERN DEMO ===\n");

        // Build an email notification with all optional fields
        Notification emailNotification = Notification.builder()
                .to("alice@company.com")
                .withMessage("Your order has shipped!")
                .viaChannel("EMAIL")
                .withSubject("Order Update")
                .withPriority(2)
                .build();
        System.out.println("Built: " + emailNotification);

        // Build an SMS notification - no subject needed
        Notification smsNotification = Notification.builder()
                .to("+1-555-0123")
                .withMessage("Your verification code is 482910")
                .viaChannel("SMS")
                .withPriority(1)
                .build();
        System.out.println("Built: " + smsNotification);

        // Build a push notification - uses default priority
        Notification pushNotification = Notification.builder()
                .to("device-token-abc123")
                .withMessage("You have a new message from Bob")
                .viaChannel("PUSH")
                .withPriority(3)
                .build();
        System.out.println("Built: " + pushNotification);

        // Send through the naive service to verify Builder produces valid objects
        System.out.println("\n--- Sending via NaiveNotificationService ---");
        NaiveNotificationService service = new NaiveNotificationService();
        service.send(emailNotification);
        service.send(smsNotification);
        service.send(pushNotification);
    }
}

What's happening here?

Compare the old constructor call:

new Notification("user@email.com", "Hello", "EMAIL", "Greeting", 1)

With the new Builder call:

Notification.builder().to("user@email.com").withMessage("Hello").viaChannel("EMAIL").withSubject("Greeting").withPriority(1).build()

Every parameter now has a name. You can skip optional fields like withSubject() entirely instead of passing null.

  • Save Main.java.
  • Run Main.java by right-clicking the file in IntelliJ IDEA and selecting Run 'Main.main()'.
  • In the console output, you should see three "Built:" lines showing each notification's details, followed by the delivery output from NaiveNotificationService.

Not seeing any output?

  • Make sure all three files (Notification.java, NaiveNotificationService.java, Main.java) have the same package declaration: com.patterns.notifications.
  • Check that you right-clicked Main.java specifically, not another file.
  • If IntelliJ shows a build error, read the error message in the Build tab at the bottom. It usually points to the exact line number.

Help me fix my Builder pattern code so Main.java runs

Understand when to use Builder in production

When should I use the Builder Pattern?

Use Builder when a class has many optional parameters, or when the constructor call becomes unreadable. This is common with configuration objects, HTTP request objects, and test data factories.

In production Java, this pattern is everywhere. Libraries like OkHttp use it for building HTTP requests. Retrofit uses it for building API clients. Lombok's @Builder annotation auto-generates this exact pattern for you.

✔️ Awesome, I've got everything!

Great. Double check you have saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete Notification.java at this point:

package com.patterns.notifications;

public class Notification {
    private final String recipient;
    private final String message;
    private final String channel;
    private final String subject;
    private final int priority;

    private Notification(Builder builder) {
        this.recipient = builder.recipient;
        this.message = builder.message;
        this.channel = builder.channel;
        this.subject = builder.subject;
        this.priority = builder.priority;
    }

    public String getRecipient() { return recipient; }
    public String getMessage() { return message; }
    public String getChannel() { return channel; }
    public String getSubject() { return subject; }
    public int getPriority() { return priority; }

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

    @Override
    public String toString() {
        return String.format("Notification{to='%s', channel='%s', priority=%d, message='%s'}",
                recipient, channel, priority, message);
    }

    public static class Builder {
        private String recipient;
        private String message;
        private String channel;
        private String subject;
        private int priority = 5;

        public Builder to(String recipient) {
            this.recipient = recipient;
            return this;
        }

        public Builder withMessage(String message) {
            this.message = message;
            return this;
        }

        public Builder viaChannel(String channel) {
            this.channel = channel;
            return this;
        }

        public Builder withSubject(String subject) {
            this.subject = subject;
            return this;
        }

        public Builder withPriority(int priority) {
            this.priority = priority;
            return this;
        }

        public Notification build() {
            if (recipient == null || message == null || channel == null) {
                throw new IllegalStateException("recipient, message, and channel are required");
            }
            return new Notification(this);
        }
    }
}

And here is the complete Main.java at this point:

package com.patterns.notifications;

public class Main {

    public static void main(String[] args) {
        System.out.println("=== BUILDER PATTERN DEMO ===\n");

        // Build an email notification with all optional fields
        Notification emailNotification = Notification.builder()
                .to("alice@company.com")
                .withMessage("Your order has shipped!")
                .viaChannel("EMAIL")
                .withSubject("Order Update")
                .withPriority(2)
                .build();
        System.out.println("Built: " + emailNotification);

        // Build an SMS notification - no subject needed
        Notification smsNotification = Notification.builder()
                .to("+1-555-0123")
                .withMessage("Your verification code is 482910")
                .viaChannel("SMS")
                .withPriority(1)
                .build();
        System.out.println("Built: " + smsNotification);

        // Build a push notification - uses default priority
        Notification pushNotification = Notification.builder()
                .to("device-token-abc123")
                .withMessage("You have a new message from Bob")
                .viaChannel("PUSH")
                .withPriority(3)
                .build();
        System.out.println("Built: " + pushNotification);

        // Send through the naive service to verify Builder produces valid objects
        System.out.println("\n--- Sending via NaiveNotificationService ---");
        NaiveNotificationService service = new NaiveNotificationService();
        service.send(emailNotification);
        service.send(smsNotification);
        service.send(pushNotification);
    }
}

Your Notification objects are now clean and readable. Next up, you will tackle the if/else chains inside NaiveNotificationService by introducing the Strategy Pattern to swap delivery algorithms at runtime.

Apply the Strategy Pattern

In the previous step, you built clean Notification objects using the Builder pattern. But the core problem from NaiveNotificationService remains: a wall of if/else chains that decide how to deliver each message.

The Strategy pattern solves this by extracting each delivery algorithm into its own class. Instead of one method that knows about every channel, you get interchangeable strategy objects that each handle one channel. The right strategy gets plugged in at runtime.

In this step, get ready to:
  • Create a DeliveryStrategy interface that defines a common delivery contract.
  • Implement three concrete strategies for Email, SMS, and Push delivery.
  • Demonstrate swapping strategies at runtime in Main.java.
Create the DeliveryStrategy interface

Every strategy pattern starts with an interface that defines the contract all algorithms must follow. In this case, every delivery channel needs to implement a single deliver() method.

  • Right-click the com.patterns.notifications package in IntelliJ's Project panel.
  • Select New, then Java Class.
  • Select Interface from the kind dropdown and name it DeliveryStrategy.
  • Replace the generated content with this code:
package com.patterns.notifications;

public interface DeliveryStrategy {
    void deliver(Notification notification);
}

What does this code do?

This interface defines a single method that all delivery strategies must implement. Any class that implements DeliveryStrategy promises it can deliver a notification. The caller doesn't need to know which channel it uses.

  • Save DeliveryStrategy.java with Ctrl+S (Windows) or Cmd+S (macOS).
  • Confirm IntelliJ shows no red underlines in the file. A clean file means the interface compiles correctly.

Seeing red underlines?

Make sure the package declaration matches your folder structure exactly: com.patterns.notifications. Also check that Notification in the method signature matches your existing class name.

Help me fix red underlines in my DeliveryStrategy interface

✔️ 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 com.patterns.notifications;

public interface DeliveryStrategy {
    void deliver(Notification notification);
}
Implement the concrete delivery strategies

Now you'll create three classes, one per channel. Each implements DeliveryStrategy and provides its own channel-specific delivery logic.

  • Right-click the com.patterns.notifications package, select New > Java Class, and name it EmailDeliveryStrategy.
  • Replace the generated content with this code:
package com.patterns.notifications;

public class EmailDeliveryStrategy implements DeliveryStrategy {
    @Override
    public void deliver(Notification notification) {
        // Fall back to "No Subject" for notifications without a subject line
        String subject = notification.getSubject() != null ? notification.getSubject() : "No Subject";
        System.out.printf("  [EMAIL] Sending to %s%n", notification.getRecipient());
        System.out.printf("  Subject: %s%n", subject);
        System.out.printf("  Body: %s%n", notification.getMessage());
        System.out.println("  Status: Delivered via SMTP");
    }
}

What does this code do?

This strategy handles email delivery specifically. It checks for an optional subject, then prints the full email structure (recipient, subject, body, status). The @Override annotation tells the compiler this method implements the interface contract.

  • Save EmailDeliveryStrategy.java.
  • Confirm no red underlines appear. IntelliJ recognizes that the class correctly implements DeliveryStrategy.

Seeing 'class must implement abstract method'?

This means the method signature doesn't match the interface. Check that your method is public void deliver(Notification notification) with the exact parameter type.

Help me fix my EmailDeliveryStrategy implementation

✔️ 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 com.patterns.notifications;

public class EmailDeliveryStrategy implements DeliveryStrategy {
    @Override
    public void deliver(Notification notification) {
        String subject = notification.getSubject() != null ? notification.getSubject() : "No Subject";
        System.out.printf("  [EMAIL] Sending to %s%n", notification.getRecipient());
        System.out.printf("  Subject: %s%n", subject);
        System.out.printf("  Body: %s%n", notification.getMessage());
        System.out.println("  Status: Delivered via SMTP");
    }
}
  • Create another new Java class in the same package and name it SmsDeliveryStrategy.
  • Replace the generated content with this code:
package com.patterns.notifications;

public class SmsDeliveryStrategy implements DeliveryStrategy {
    @Override
    public void deliver(Notification notification) {
        // SMS messages are limited to 160 characters, so truncate if needed
        String truncated = notification.getMessage().length() > 160
                ? notification.getMessage().substring(0, 157) + "..."
                : notification.getMessage();
        System.out.printf("  [SMS] Sending to %s%n", notification.getRecipient());
        System.out.printf("  Message: %s%n", truncated);
        System.out.println("  Status: Delivered via SMS Gateway");
    }
}

What does this code do?

This strategy handles SMS-specific logic. Real SMS messages have a 160-character limit, so the strategy truncates long messages and appends an ellipsis. Each strategy encapsulates the rules for its own channel.

  • Save SmsDeliveryStrategy.java.
  • Confirm no red underlines appear in the file.

✔️ Awesome, I've got everything!

Great. One more strategy to go.

ⓧ I'd like to double check the full code

package com.patterns.notifications;

public class SmsDeliveryStrategy implements DeliveryStrategy {
    @Override
    public void deliver(Notification notification) {
        String truncated = notification.getMessage().length() > 160
                ? notification.getMessage().substring(0, 157) + "..."
                : notification.getMessage();
        System.out.printf("  [SMS] Sending to %s%n", notification.getRecipient());
        System.out.printf("  Message: %s%n", truncated);
        System.out.println("  Status: Delivered via SMS Gateway");
    }
}
  • Create one more Java class in the same package and name it PushDeliveryStrategy.
  • Replace the generated content with this code:
package com.patterns.notifications;

public class PushDeliveryStrategy implements DeliveryStrategy {
    @Override
    public void deliver(Notification notification) {
        // Push notifications target device tokens, not email addresses or phone numbers
        System.out.printf("  [PUSH] Sending to device token: %s%n", notification.getRecipient());
        System.out.printf("  Alert: %s%n", notification.getMessage());
        System.out.println("  Status: Delivered via Push Service");
    }
}

What does this code do?

This strategy handles push notification delivery. It treats the recipient as a device token and formats the output as an alert. Notice how each strategy has completely different formatting logic, but they all satisfy the same DeliveryStrategy contract.

  • Save PushDeliveryStrategy.java.
  • Confirm no red underlines appear in the file.

Red underlines in any of the strategy files?

Check that each file starts with package com.patterns.notifications; and that implements DeliveryStrategy appears in the class declaration. IntelliJ also requires that the filename matches the class name exactly (case-sensitive).

Help me fix my delivery strategy implementations

✔️ Awesome, I've got everything!

Great. All three strategy classes are saved and ready to use.

ⓧ I'd like to double check the full code

package com.patterns.notifications;

public class PushDeliveryStrategy implements DeliveryStrategy {
    @Override
    public void deliver(Notification notification) {
        System.out.printf("  [PUSH] Sending to device token: %s%n", notification.getRecipient());
        System.out.printf("  Alert: %s%n", notification.getMessage());
        System.out.println("  Status: Delivered via Push Service");
    }
}
Demonstrate the Strategy pattern in Main.java

Your three strategies are ready. Now you'll use them in Main.java to prove that the same notification can be delivered by different strategies without any if/else logic.

  • Open Main.java in IntelliJ.
  • Find the closing System.out.println("Built: " + pushNotification); line from the Builder demo.
  • Add the following Strategy pattern demo below it:
        // --- Pattern 2: Strategy ---
        System.out.println("\n--- STRATEGY PATTERN ---");
        System.out.println("Delivering email with EmailDeliveryStrategy:");
        DeliveryStrategy emailStrategy = new EmailDeliveryStrategy();
        emailStrategy.deliver(emailNotification);

        System.out.println("\nDelivering SMS with SmsDeliveryStrategy:");
        DeliveryStrategy smsStrategy = new SmsDeliveryStrategy();
        smsStrategy.deliver(smsNotification);

What does this code do?

Both emailStrategy and smsStrategy are declared as DeliveryStrategy (the interface type), not their concrete class type. This means the calling code doesn't care which channel it's dealing with. You could swap in any strategy without changing a single line here.

Compare this to the NaiveNotificationService where adding a new channel meant adding another if/else branch. With Strategy, you just create a new class.

  • Save Main.java.
  • Run the program by right-clicking Main.java in the Project panel and selecting Run 'Main.main()'.
  • In the console output, you should see the Builder section followed by a --- STRATEGY PATTERN --- header, then the formatted email delivery and SMS delivery output.

Program won't run or shows compile errors?

Check that the new code sits inside the main() method's curly braces, not outside of them. Also confirm that emailNotification and smsNotification are defined earlier in the same method from your Builder demo.

Help me fix compile errors in my Main.java Strategy demo

Where is Strategy used in production?

Payment processing systems use Strategy to swap between Card, PayPal, and Crypto payment methods. Authentication systems use it to switch between OAuth, SAML, and JWT validation. Compression libraries use it to swap between algorithms.

The key benefit is elimination of if/else chains. Adding a new channel (like Slack) means creating one new class. No existing code changes.

✔️ Awesome, I've got everything!

Great. Double check you've saved all files before moving on.

ⓧ I'd like to double check the full code

package com.patterns.notifications;

public class Main {

    public static void main(String[] args) {
        System.out.println("=== NOTIFICATION SERVICE - DESIGN PATTERNS DEMO ===\n");

        // --- Pattern 1: Builder ---
        System.out.println("--- BUILDER PATTERN ---");
        Notification emailNotification = Notification.builder()
                .to("alice@company.com")
                .withMessage("Your order has shipped!")
                .viaChannel("EMAIL")
                .withSubject("Order Update")
                .withPriority(2)
                .build();
        System.out.println("Built: " + emailNotification);

        Notification smsNotification = Notification.builder()
                .to("+1-555-0123")
                .withMessage("Your verification code is 482910")
                .viaChannel("SMS")
                .withPriority(1)
                .build();
        System.out.println("Built: " + smsNotification);

        Notification pushNotification = Notification.builder()
                .to("device-token-abc123")
                .withMessage("You have a new message from Bob")
                .viaChannel("PUSH")
                .withPriority(3)
                .build();
        System.out.println("Built: " + pushNotification);

        // --- Pattern 2: Strategy ---
        System.out.println("\n--- STRATEGY PATTERN ---");
        System.out.println("Delivering email with EmailDeliveryStrategy:");
        DeliveryStrategy emailStrategy = new EmailDeliveryStrategy();
        emailStrategy.deliver(emailNotification);

        System.out.println("\nDelivering SMS with SmsDeliveryStrategy:");
        DeliveryStrategy smsStrategy = new SmsDeliveryStrategy();
        smsStrategy.deliver(smsNotification);

        System.out.println("\n=== DEMO COMPLETE ===");
    }
}

Your delivery strategies work, but right now you're manually choosing which strategy to instantiate. In the next step, you'll build a Factory that automatically picks the right strategy based on the channel name. No more hardcoding types.

Apply the Factory Pattern

In the previous step, you replaced if/else chains with interchangeable Strategy objects. But look at your Main.java code. You're still writing new EmailDeliveryStrategy() and new SmsDeliveryStrategy() directly. The client code knows exactly which class to create for each channel.

The Factory Pattern solves this by centralizing object creation in one place. Your client code asks for a strategy by channel name, and the factory returns the correct implementation. Adding a new channel later means updating one factory class instead of hunting through every file that creates strategies.

In this step, get ready to:
  • Create a factory class that returns the correct delivery strategy based on channel name.
  • Update Main.java to use the factory instead of manually instantiating strategy classes.
Create the DeliveryStrategyFactory class

The factory uses a Java switch expression (finalized in Java 14, fully available in JDK 21) for clean routing. It takes a channel name as a string and returns the matching DeliveryStrategy implementation.

  • In IntelliJ IDEA, right-click the com.patterns.notifications package in the Project sidebar.
  • Select New, then Java Class.
  • Name it DeliveryStrategyFactory and press Enter.
  • Replace the generated contents with the following code:
package com.patterns.notifications;

public class DeliveryStrategyFactory {

    public static DeliveryStrategy getStrategy(String channel) {
        // Switch expression returns the correct strategy based on channel name
        return switch (channel.toUpperCase()) {
            case "EMAIL" -> new EmailDeliveryStrategy();
            case "SMS" -> new SmsDeliveryStrategy();
            case "PUSH" -> new PushDeliveryStrategy();
            default -> throw new IllegalArgumentException("Unknown channel: " + channel);
        };
    }
}

What does this code do?

The getStrategy() method is static, so you call it directly on the class without creating a factory instance. It takes any channel string, converts it to uppercase for case-insensitive matching, and returns the correct strategy.

The switch expression (the -> arrow syntax) evaluates to a value directly. If the channel doesn't match any known type, it throws an IllegalArgumentException to fail fast with a clear error message.

  • Save DeliveryStrategyFactory.java.

Why Factory and Strategy work together

The Strategy pattern defines interchangeable algorithms behind a common interface. The Factory pattern decides which algorithm to use based on runtime input. Together, they eliminate both the if/else delivery logic (Strategy) and the if/else creation logic (Factory).

Seeing a red underline on the switch expression?

Make sure your project SDK is set to JDK 21. Go to File > Project Structure > Project and confirm the SDK is 21 and Language Level is 21 or higher.

help me fix a switch expression error in my Java Factory class

Use the factory in Main.java

Now the client code can request a strategy by channel name without knowing which concrete class implements it. The factory handles all the creation logic internally.

  • Open Main.java in IntelliJ IDEA.
  • Find the end of the Strategy pattern demo section (after smsStrategy.deliver(smsNotification)).
  • Add the following Factory pattern demo below it:
        // --- Pattern 3: Factory ---
        System.out.println("\n--- FACTORY PATTERN ---");
        System.out.println("Getting strategy from factory for 'PUSH':");
        DeliveryStrategy factoryStrategy = DeliveryStrategyFactory.getStrategy("PUSH");
        factoryStrategy.deliver(pushNotification);

What does this code do?

Instead of writing new PushDeliveryStrategy(), you call DeliveryStrategyFactory.getStrategy("PUSH"). The client code only knows about the DeliveryStrategy interface. It never references PushDeliveryStrategy directly.

If you add a new channel later (like Slack), you update the factory's switch expression and nothing else changes in the client code.

  • Save Main.java.
  • Right-click inside Main.java and select Run 'Main.main()'.
  • In the console output, look for the --- FACTORY PATTERN --- section. You should see [PUSH] delivery output for the push notification, created entirely by the factory.

Getting an IllegalArgumentException?

This means the channel string you passed to getStrategy() doesn't match EMAIL, SMS, or PUSH. Check for typos in your viaChannel() call when building the notification.

help me fix the IllegalArgumentException in my Factory pattern code

✔️ Awesome, I've got everything!

Great. Double check you've saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete DeliveryStrategyFactory.java:

package com.patterns.notifications;

public class DeliveryStrategyFactory {

    public static DeliveryStrategy getStrategy(String channel) {
        return switch (channel.toUpperCase()) {
            case "EMAIL" -> new EmailDeliveryStrategy();
            case "SMS" -> new SmsDeliveryStrategy();
            case "PUSH" -> new PushDeliveryStrategy();
            default -> throw new IllegalArgumentException("Unknown channel: " + channel);
        };
    }
}
When to use Factory in production

The Factory pattern appears everywhere in Java's standard library and popular frameworks. Understanding when it fits helps you recognize it in codebases you'll work with.

When does Factory earn its place?

  • Object creation depends on runtime input (user selection, config files, API responses). Your code won't know the type until execution.
  • You want to hide concrete implementations behind an interface. Client code works with DeliveryStrategy without importing EmailDeliveryStrategy.
  • Adding new types should not require changing client code. Only the factory needs an update.

Real-world examples include Calendar.getInstance() in the JDK, JDBC's DriverManager.getConnection(), and Spring's BeanFactory.

You now have three patterns working together. Builder constructs notifications cleanly, Strategy delivers them through interchangeable algorithms, and Factory creates the right strategy without the client knowing concrete types. Next up, you'll add behavior like logging and retry to your strategies without modifying a single line of existing code.

Apply the Decorator Pattern

Your Factory pattern now creates the right delivery strategy without any client code knowing the concrete class. But what happens when you need to add logging to every delivery? Or automatic retries on failure?

Editing each strategy class directly would violate the Open/Closed Principle you worked so hard to achieve. The Decorator pattern solves this by wrapping existing objects with new behavior. You layer functionality on top without touching the original code.

In this step, get ready to:
  • Create a base decorator class that wraps any delivery strategy.
  • Build logging and retry decorators that add behavior around delivery.
  • Stack multiple decorators together and observe the layered output.
Create the base decorator class

The Decorator pattern starts with an abstract class that implements the same interface as the objects it wraps. This lets decorators stand in for any strategy transparently.

  • Create a new Java class file called DeliveryStrategyDecorator.java in your com.patterns.notifications package. Right-click the package in the Project sidebar, select New, then Java Class.
  • Name the class DeliveryStrategyDecorator and add the following code:
package com.patterns.notifications;

public abstract class DeliveryStrategyDecorator implements DeliveryStrategy {
    // Hold a reference to the strategy being wrapped
    protected final DeliveryStrategy wrapped;

    public DeliveryStrategyDecorator(DeliveryStrategy wrapped) {
        this.wrapped = wrapped;
    }

    // Default behavior: just delegate to the wrapped strategy
    @Override
    public void deliver(Notification notification) {
        wrapped.deliver(notification);
    }
}

What does this code do?

This abstract class implements DeliveryStrategy so it can stand in anywhere a strategy is expected. It stores a reference to the wrapped strategy and delegates deliver() to it by default.

Subclasses override deliver() to add their own behavior before or after calling the wrapped strategy. This is composition over inheritance in action.

  • Save DeliveryStrategyDecorator.java.
  • You should see no compile errors in the editor. The class appears in the Project sidebar alongside your strategy classes.

Seeing a compile error?

Make sure the package declaration at the top matches your other files exactly: com.patterns.notifications. Also confirm the class is abstract. Without that keyword, Java expects you to be instantiating it directly.

help me fix a compile error in my DeliveryStrategyDecorator class

✔️ Awesome, I've got everything!

Great. Make sure the file is saved before moving on.

ⓧ I'd like to double check the full code

package com.patterns.notifications;

public abstract class DeliveryStrategyDecorator implements DeliveryStrategy {
    // Hold a reference to the strategy being wrapped
    protected final DeliveryStrategy wrapped;

    public DeliveryStrategyDecorator(DeliveryStrategy wrapped) {
        this.wrapped = wrapped;
    }

    // Default behavior: just delegate to the wrapped strategy
    @Override
    public void deliver(Notification notification) {
        wrapped.deliver(notification);
    }
}
Create the logging decorator

The first concrete decorator adds timestamped logging before and after every delivery. This is useful for debugging and audit trails without modifying any strategy class.

  • Create a new Java class called LoggingDecorator.java in the same package. Add the following code:
package com.patterns.notifications;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class LoggingDecorator extends DeliveryStrategyDecorator {

    public LoggingDecorator(DeliveryStrategy wrapped) {
        super(wrapped);
    }

    @Override
    public void deliver(Notification notification) {
        // Format current time for the log message
        String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        System.out.printf("  [LOG %s] Starting delivery to %s via %s%n",
                timestamp, notification.getRecipient(), notification.getChannel());
        // Delegate to the wrapped strategy
        wrapped.deliver(notification);
        System.out.printf("  [LOG %s] Delivery completed%n", timestamp);
    }
}

What does this code do?

The LoggingDecorator captures a timestamp using Java's LocalDateTime and prints a log line before and after calling the wrapped strategy's deliver() method.

The key insight: EmailDeliveryStrategy has no idea it's being logged. The decorator adds behavior externally.

  • Save LoggingDecorator.java.
  • Confirm no red underlines appear in the editor. The extends DeliveryStrategyDecorator should resolve without issues.

Red underline on the imports?

If LocalDateTime or DateTimeFormatter show errors, make sure both import statements are present at the top of the file. IntelliJ can auto-import with Alt+Enter (Windows) or Option+Enter (macOS) when your cursor is on the red text.

help me fix import errors in my LoggingDecorator

✔️ Awesome, I've got everything!

Great. Make sure the file is saved before moving on.

ⓧ I'd like to double check the full code

package com.patterns.notifications;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class LoggingDecorator extends DeliveryStrategyDecorator {

    public LoggingDecorator(DeliveryStrategy wrapped) {
        super(wrapped);
    }

    @Override
    public void deliver(Notification notification) {
        // Format current time for the log message
        String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        System.out.printf("  [LOG %s] Starting delivery to %s via %s%n",
                timestamp, notification.getRecipient(), notification.getChannel());
        // Delegate to the wrapped strategy
        wrapped.deliver(notification);
        System.out.printf("  [LOG %s] Delivery completed%n", timestamp);
    }
}
Create the retry decorator

Network calls fail. The second decorator adds automatic retry logic with exponential backoff. If delivery fails, it waits progressively longer between attempts before giving up.

  • Create a new Java class called RetryDecorator.java in the same package. Add the following code:
package com.patterns.notifications;

import java.util.Random;

public class RetryDecorator extends DeliveryStrategyDecorator {
    private final int maxRetries;
    private final Random random = new Random();

    public RetryDecorator(DeliveryStrategy wrapped, int maxRetries) {
        super(wrapped);
        this.maxRetries = maxRetries;
    }

    @Override
    public void deliver(Notification notification) {
        for (int attempt = 1; attempt <= maxRetries; attempt++) {
            try {
                // Simulate random failure on retries
                if (attempt > 1 && random.nextInt(10) < 3) {
                    throw new RuntimeException("Simulated network failure");
                }
                wrapped.deliver(notification);
                return;
            } catch (RuntimeException e) {
                System.out.printf("  [RETRY] Attempt %d/%d failed: %s%n", attempt, maxRetries, e.getMessage());
                if (attempt == maxRetries) {
                    System.out.println("  [RETRY] All attempts exhausted. Delivery failed.");
                } else {
                    // Exponential backoff: 200ms, 400ms, 800ms...
                    long backoff = (long) Math.pow(2, attempt) * 100;
                    System.out.printf("  [RETRY] Waiting %dms before retry...%n", backoff);
                    try { Thread.sleep(backoff); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); }
                }
            }
        }
    }
}

What does this code do?

The retry loop attempts delivery up to maxRetries times. On the first attempt, it delivers normally. On subsequent attempts, a simulated 30% failure rate (random.nextInt(10) < 3) triggers a RuntimeException to mimic network issues.

The exponential backoff doubles the wait each time: 200ms, 400ms, 800ms. This prevents hammering a failing service. If all attempts fail, it logs the exhaustion and gives up gracefully.

  • Save RetryDecorator.java.
  • Verify no compile errors appear. The Random import from java.util should resolve automatically.

Error on Thread.sleep?

Make sure the Thread.sleep(backoff) call is wrapped in its own try/catch for InterruptedException. Java requires checked exceptions to be handled. The code handles it inline on the same line.

help me fix a Thread.sleep compile error in RetryDecorator

✔️ Awesome, I've got everything!

Great. Make sure the file is saved before moving on.

ⓧ I'd like to double check the full code

package com.patterns.notifications;

import java.util.Random;

public class RetryDecorator extends DeliveryStrategyDecorator {
    private final int maxRetries;
    private final Random random = new Random();

    public RetryDecorator(DeliveryStrategy wrapped, int maxRetries) {
        super(wrapped);
        this.maxRetries = maxRetries;
    }

    @Override
    public void deliver(Notification notification) {
        for (int attempt = 1; attempt <= maxRetries; attempt++) {
            try {
                // Simulate random failure on retries
                if (attempt > 1 && random.nextInt(10) < 3) {
                    throw new RuntimeException("Simulated network failure");
                }
                wrapped.deliver(notification);
                return;
            } catch (RuntimeException e) {
                System.out.printf("  [RETRY] Attempt %d/%d failed: %s%n", attempt, maxRetries, e.getMessage());
                if (attempt == maxRetries) {
                    System.out.println("  [RETRY] All attempts exhausted. Delivery failed.");
                } else {
                    // Exponential backoff: 200ms, 400ms, 800ms...
                    long backoff = (long) Math.pow(2, attempt) * 100;
                    System.out.printf("  [RETRY] Waiting %dms before retry...%n", backoff);
                    try { Thread.sleep(backoff); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); }
                }
            }
        }
    }
}
Stack decorators in Main.java

The real power of the Decorator pattern is stacking. You can wrap a strategy with logging, then wrap that with retry. Each layer adds its behavior independently.

  • Open Main.java and find the Factory pattern section at the bottom of your main method. Add the following code below it:
        // --- Pattern 4: Decorator ---
        System.out.println("\n--- DECORATOR PATTERN ---");
        System.out.println("Wrapping EmailStrategy with Logging and Retry:");
        DeliveryStrategy decorated = new LoggingDecorator(
                new RetryDecorator(new EmailDeliveryStrategy(), 3)
        );
        decorated.deliver(emailNotification);

What does this code do?

This creates a layered object: an EmailDeliveryStrategy at the core, wrapped with RetryDecorator (3 max attempts), then wrapped again with LoggingDecorator. When deliver() is called, execution flows: Logging (before) > Retry > Email > Logging (after).

The original EmailDeliveryStrategy code is completely untouched. You added logging and retry purely through composition.

  • Save Main.java.
  • Run Main.java by right-clicking the file in the Project sidebar and selecting Run 'Main.main()'.
  • In the console output, scroll to the DECORATOR PATTERN section. You should see [LOG] lines with timestamps surrounding the email delivery output.

Not seeing LOG lines in the output?

Make sure you wrapped in the correct order. The outermost decorator runs first. Check that LoggingDecorator is the outer wrapper and RetryDecorator is the inner one. Also verify you are calling decorated.deliver(emailNotification) and not emailStrategy.deliver(emailNotification).

help me debug why my stacked decorators are not producing the expected output

Where is this pattern used in production?

Java's own I/O library uses this exact pattern. When you write new BufferedInputStream(new FileInputStream(file)), BufferedInputStream is a decorator that adds buffering to FileInputStream.

Spring Security filter chains work the same way. Each filter wraps the next, adding authentication, CORS checks, or logging without modifying the core request handler.

✔️ Awesome, I've got everything!

Great. Double check you have saved all files.

ⓧ I'd like to double check the full code

package com.patterns.notifications;

public class Main {

    public static void main(String[] args) {
        System.out.println("=== NOTIFICATION SERVICE - DESIGN PATTERNS DEMO ===\n");

        // --- Pattern 1: Builder ---
        System.out.println("--- BUILDER PATTERN ---");
        Notification emailNotification = Notification.builder()
                .to("alice@company.com")
                .withMessage("Your order has shipped!")
                .viaChannel("EMAIL")
                .withSubject("Order Update")
                .withPriority(2)
                .build();
        System.out.println("Built: " + emailNotification);

        Notification smsNotification = Notification.builder()
                .to("+1-555-0123")
                .withMessage("Your verification code is 482910")
                .viaChannel("SMS")
                .withPriority(1)
                .build();
        System.out.println("Built: " + smsNotification);

        Notification pushNotification = Notification.builder()
                .to("device-token-abc123")
                .withMessage("You have a new message from Bob")
                .viaChannel("PUSH")
                .withPriority(3)
                .build();
        System.out.println("Built: " + pushNotification);

        // --- Pattern 2: Strategy ---
        System.out.println("\n--- STRATEGY PATTERN ---");
        System.out.println("Delivering email with EmailDeliveryStrategy:");
        DeliveryStrategy emailStrategy = new EmailDeliveryStrategy();
        emailStrategy.deliver(emailNotification);

        System.out.println("\nDelivering SMS with SmsDeliveryStrategy:");
        DeliveryStrategy smsStrategy = new SmsDeliveryStrategy();
        smsStrategy.deliver(smsNotification);

        // --- Pattern 3: Factory ---
        System.out.println("\n--- FACTORY PATTERN ---");
        System.out.println("Getting strategy from factory for 'PUSH':");
        DeliveryStrategy factoryStrategy = DeliveryStrategyFactory.getStrategy("PUSH");
        factoryStrategy.deliver(pushNotification);

        // --- Pattern 4: Decorator ---
        System.out.println("\n--- DECORATOR PATTERN ---");
        System.out.println("Wrapping EmailStrategy with Logging and Retry:");
        DeliveryStrategy decorated = new LoggingDecorator(
                new RetryDecorator(new EmailDeliveryStrategy(), 3)
        );
        decorated.deliver(emailNotification);
    }
}

Your strategies can now gain new behaviors without a single line of code changing inside them. Next up, you will use the Observer pattern to broadcast events to multiple listeners whenever a notification is delivered.

Apply the Observer Pattern

Your notification delivery now has logging and automatic retry, all added without touching the original strategy code.

When a notification goes out, other parts of the system need to react. The Observer pattern lets multiple independent listeners subscribe to events without the sender knowing anything about them.

In this step, get ready to:
  • Create an event listener interface and three concrete listeners.
  • Build an event publisher that broadcasts notifications to subscribers.
  • Demonstrate the Observer pattern in your demo class.
Create the event listener interface and concrete listeners

The Observer pattern starts with a contract. Listeners agree to implement a specific method, and the publisher calls that method whenever an event occurs.

  • In IntelliJ IDEA, right-click your com.patterns.notifications package in the Project sidebar.
  • Select New then Java Class.
  • Name it NotificationEventListener and select Interface from the kind dropdown.
  • Replace the file contents with this code:
package com.patterns.notifications;

public interface NotificationEventListener {
    void onNotificationSent(Notification notification);
}

What does this interface define?

Any class that implements NotificationEventListener must provide a onNotificationSent() method. This is the single contract that all listeners agree to follow.

  • Create a new Java class in the same package named AuditLogger.
  • Replace the file contents with this code:
package com.patterns.notifications;

public class AuditLogger implements NotificationEventListener {
    @Override
    public void onNotificationSent(Notification notification) {
        System.out.printf("  [AUDIT] Logged: notification sent to %s via %s%n",
                notification.getRecipient(), notification.getChannel());
    }
}

What does this listener do?

AuditLogger records that a notification was sent. In a production system, this would write to a persistent audit trail for compliance tracking.

  • Create another new Java class named AnalyticsTracker.
  • Replace the file contents with this code:
package com.patterns.notifications;

public class AnalyticsTracker implements NotificationEventListener {
    @Override
    public void onNotificationSent(Notification notification) {
        System.out.printf("  [ANALYTICS] Tracked: %s channel delivery, priority=%d%n",
                notification.getChannel(), notification.getPriority());
    }
}

What does this listener track?

AnalyticsTracker records which channel was used and the message priority. In production, this feeds dashboards showing delivery volume and channel distribution.

  • Create one more Java class named UserActivityUpdater.
  • Replace the file contents with this code:
package com.patterns.notifications;

public class UserActivityUpdater implements NotificationEventListener {
    @Override
    public void onNotificationSent(Notification notification) {
        System.out.printf("  [ACTIVITY] Updated last-contact for %s%n", notification.getRecipient());
    }
}

What does this listener update?

UserActivityUpdater refreshes the user's last-contact timestamp. In production, this keeps CRM records current so support teams know when a customer was last reached.

  • Save all four files (NotificationEventListener.java, AuditLogger.java, AnalyticsTracker.java, UserActivityUpdater.java).

You won't see output yet. These listeners come alive once you wire them to a publisher in the next substep.

Seeing red underlines in your listener classes?

Make sure NotificationEventListener.java is saved and in the same package (com.patterns.notifications). IntelliJ needs the interface file saved before it can resolve the implements clause in the listener classes.

help me fix red underlines in my Observer listener classes

Build the event publisher

The publisher maintains a list of subscribers and notifies all of them when an event occurs. Listeners can subscribe or unsubscribe at any time without the publisher caring who they are.

  • Create a new Java class named NotificationEventPublisher in the same package.
  • Replace the file contents with this code:
package com.patterns.notifications;

import java.util.ArrayList;
import java.util.List;

public class NotificationEventPublisher {
    private final List<NotificationEventListener> listeners = new ArrayList<>();

    public void subscribe(NotificationEventListener listener) {
        listeners.add(listener);
    }

    public void unsubscribe(NotificationEventListener listener) {
        listeners.remove(listener);
    }

    public void notifyListeners(Notification notification) {
        for (NotificationEventListener listener : listeners) {
            listener.onNotificationSent(notification);
        }
    }
}

What does this code do?

The publisher holds an ArrayList of listeners. Any class implementing NotificationEventListener can subscribe or unsubscribe at runtime.

When notifyListeners() is called, it loops through every subscriber and invokes their onNotificationSent() method. The publisher has zero knowledge of what each listener does with the event.

  • Save NotificationEventPublisher.java.

You won't see a change yet. The next substep wires the publisher into your demo to produce visible output.

Unresolved symbol on List or ArrayList?

Check that your import statements at the top of the file include both java.util.ArrayList and java.util.List. IntelliJ usually auto-imports, but if you pasted the code, the imports may not have triggered.

help me resolve missing imports in my NotificationEventPublisher class

✔️ Awesome, I've got everything!

Great. Make sure the file is saved before moving on.

ⓧ I'd like to double check the full code

package com.patterns.notifications;

import java.util.ArrayList;
import java.util.List;

public class NotificationEventPublisher {
    private final List<NotificationEventListener> listeners = new ArrayList<>();

    public void subscribe(NotificationEventListener listener) {
        listeners.add(listener);
    }

    public void unsubscribe(NotificationEventListener listener) {
        listeners.remove(listener);
    }

    public void notifyListeners(Notification notification) {
        for (NotificationEventListener listener : listeners) {
            listener.onNotificationSent(notification);
        }
    }
}
Demonstrate the Observer pattern in Main.java

With the interface, listeners, and publisher in place, you can now wire them together. You will create a publisher, subscribe all three listeners, and broadcast an event.

  • Open Main.java.
  • Find the closing braces at the bottom of the main method (the last two } lines).
  • Add the following code just above those closing braces, after the Decorator pattern section:
        // --- Pattern 5: Observer ---
        System.out.println("\n--- OBSERVER PATTERN ---");
        NotificationEventPublisher publisher = new NotificationEventPublisher();
        publisher.subscribe(new AuditLogger());
        publisher.subscribe(new AnalyticsTracker());
        publisher.subscribe(new UserActivityUpdater());
        System.out.println("Broadcasting event to all listeners:");
        publisher.notifyListeners(emailNotification);

What does this code do?

You create a NotificationEventPublisher and register all three listeners with subscribe(). The publisher now has three subscribers in its internal list.

When you call notifyListeners(emailNotification), the publisher iterates through all subscribers and calls each one's onNotificationSent() method. Each listener reacts independently without knowing the others exist.

  • Save Main.java.
  • Right-click inside Main.java and select Run 'Main.main()'.

At the bottom of your console output, you should see the Observer section with all three listeners printing their responses.

What should I see?

After the Decorator section output, your console should show:

--- OBSERVER PATTERN ---
Broadcasting event to all listeners:
  [AUDIT] Logged: notification sent to alice@company.com via EMAIL
  [ANALYTICS] Tracked: EMAIL channel delivery, priority=2
  [ACTIVITY] Updated last-contact for alice@company.com

All three listeners fired from a single notifyListeners() call. The publisher didn't need to know what each listener does.

Not seeing Observer output?

Make sure you added the Observer code inside the main method, not after its closing brace. The code should be indented at the same level as the Decorator section above it.

If you see a compile error about NotificationEventPublisher not being found, verify the class is saved and in the com.patterns.notifications package.

help me debug why my Observer pattern code isn't running in Main.java

✔️ Awesome, I've got everything!

Great. Double check you've saved Main.java before continuing.

ⓧ I'd like to double check the full code

package com.patterns.notifications;

public class Main {

    public static void main(String[] args) {
        System.out.println("=== NOTIFICATION SERVICE - DESIGN PATTERNS DEMO ===\n");

        // --- Pattern 1: Builder ---
        System.out.println("--- BUILDER PATTERN ---");
        Notification emailNotification = Notification.builder()
                .to("alice@company.com")
                .withMessage("Your order has shipped!")
                .viaChannel("EMAIL")
                .withSubject("Order Update")
                .withPriority(2)
                .build();
        System.out.println("Built: " + emailNotification);

        Notification smsNotification = Notification.builder()
                .to("+1-555-0123")
                .withMessage("Your verification code is 482910")
                .viaChannel("SMS")
                .withPriority(1)
                .build();
        System.out.println("Built: " + smsNotification);

        Notification pushNotification = Notification.builder()
                .to("device-token-abc123")
                .withMessage("You have a new message from Bob")
                .viaChannel("PUSH")
                .withPriority(3)
                .build();
        System.out.println("Built: " + pushNotification);

        // --- Pattern 2: Strategy ---
        System.out.println("\n--- STRATEGY PATTERN ---");
        System.out.println("Delivering email with EmailDeliveryStrategy:");
        DeliveryStrategy emailStrategy = new EmailDeliveryStrategy();
        emailStrategy.deliver(emailNotification);

        System.out.println("\nDelivering SMS with SmsDeliveryStrategy:");
        DeliveryStrategy smsStrategy = new SmsDeliveryStrategy();
        smsStrategy.deliver(smsNotification);

        // --- Pattern 3: Factory ---
        System.out.println("\n--- FACTORY PATTERN ---");
        System.out.println("Getting strategy from factory for 'PUSH':");
        DeliveryStrategy factoryStrategy = DeliveryStrategyFactory.getStrategy("PUSH");
        factoryStrategy.deliver(pushNotification);

        // --- Pattern 4: Decorator ---
        System.out.println("\n--- DECORATOR PATTERN ---");
        System.out.println("Wrapping EmailStrategy with Logging and Retry:");
        DeliveryStrategy decorated = new LoggingDecorator(
                new RetryDecorator(new EmailDeliveryStrategy(), 3)
        );
        decorated.deliver(emailNotification);

        // --- Pattern 5: Observer ---
        System.out.println("\n--- OBSERVER PATTERN ---");
        NotificationEventPublisher publisher = new NotificationEventPublisher();
        publisher.subscribe(new AuditLogger());
        publisher.subscribe(new AnalyticsTracker());
        publisher.subscribe(new UserActivityUpdater());
        System.out.println("Broadcasting event to all listeners:");
        publisher.notifyListeners(emailNotification);
    }
}

Where is the Observer pattern used in production?

Spring's ApplicationEventPublisher uses this exact pattern. You annotate a method with @EventListener and Spring automatically subscribes it to events.

DOM event listeners in JavaScript, Kafka consumers, and webhook systems all follow the same principle. One producer, many independent consumers, zero coupling between them.

Your notification system can now broadcast events to any number of listeners. Next up, you will combine all five patterns into a single cohesive service.

Bring It All Together

You now have five design patterns implemented as independent classes. Each one solves a specific problem, but in a real system, these patterns work together as a coordinated whole.

In this step, you will create a NotificationService facade class that orchestrates all five patterns behind a single, simple interface. Then you will write a comprehensive demo that shows the full lifecycle of building, routing, decorating, and broadcasting notifications.

In this step, get ready to:
  • Create a NotificationService facade that combines all five patterns.
  • Write a full demo that sends notifications through every channel.
  • Add a pattern summary reference to the top of Main.java.
Create the NotificationService facade class

A facade is a class that provides a simplified interface to a complex subsystem. Your NotificationService will hide all the pattern wiring so that callers only need one method: send(recipient, message, channel).

  • In IntelliJ IDEA, right-click the com.patterns.notifications package in the Project panel on the left.
  • Select New, then Java Class.
  • Name it NotificationService and press Enter.
  • Replace the generated content with this code:
package com.patterns.notifications;

public class NotificationService {
    private final NotificationEventPublisher eventPublisher;

    public NotificationService() {
        this.eventPublisher = new NotificationEventPublisher();
        this.eventPublisher.subscribe(new AuditLogger());
        this.eventPublisher.subscribe(new AnalyticsTracker());
        this.eventPublisher.subscribe(new UserActivityUpdater());
    }

    public void send(String recipient, String message, String channel) {
        // Use Builder to construct the notification
        Notification notification = Notification.builder()
                .to(recipient)
                .withMessage(message)
                .viaChannel(channel)
                .build();

        // Use Factory to get the right strategy for this channel
        DeliveryStrategy strategy = DeliveryStrategyFactory.getStrategy(channel);
        // Use Decorator to add logging and retry behavior
        DeliveryStrategy decorated = new LoggingDecorator(new RetryDecorator(strategy, 3));

        System.out.println("\n--- Sending Notification ---");
        System.out.println("  " + notification);
        decorated.deliver(notification);

        // Use Observer to broadcast the event to all listeners
        System.out.println("  --- Event Listeners ---");
        eventPublisher.notifyListeners(notification);
        System.out.println("--- Done ---\n");
    }

    public NotificationEventPublisher getEventPublisher() {
        return eventPublisher;
    }
}

What does this code do?

This single class combines all five patterns into one simple interface.

  • The constructor sets up the Observer pattern by creating a publisher and subscribing all three listeners.
  • The send() method uses Builder to construct the notification, Factory to pick the strategy, Decorator to wrap it with logging and retry, then Observer to broadcast the delivery event.
  • Callers never need to know about any of this wiring. They just call send() with three strings.
  • Save NotificationService.java by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Confirm there are no red underlines in the editor. Every class referenced here already exists in your package.

Seeing red underlines?

  • Check the package declaration at the top matches com.patterns.notifications exactly.
  • Make sure NotificationEventPublisher.java, AuditLogger.java, AnalyticsTracker.java, and UserActivityUpdater.java all exist in the same package.

Why is NotificationService showing unresolved references?

Write the full service demo in Main.java

Your Main.java already demonstrates each pattern individually. Now you will add a final section that shows all five patterns working together through the NotificationService facade.

  • Open Main.java in the editor.
  • Scroll to the bottom of the main() method, just before the closing } brace.
  • Find this line from the Observer demo section:
        publisher.notifyListeners(emailNotification);
  • Add the following code below that line to create the full service demo:
        // --- All Together: NotificationService Facade ---
        System.out.println("\n=== FULL SERVICE (ALL PATTERNS) ===");
        NotificationService service = new NotificationService();
        service.send("alice@company.com", "Welcome to our platform!", "EMAIL");
        service.send("+1-555-0456", "Your appointment is tomorrow at 2pm", "SMS");
        service.send("device-token-xyz789", "New friend request from Carol", "PUSH");

        System.out.println("=== DEMO COMPLETE ===");

What does this code do?

This section creates a NotificationService instance and sends three notifications through different channels. Behind that simple send() call, all five patterns are firing.

Builder constructs each notification object. Factory picks the right delivery strategy. Decorator wraps it with logging and retry. Observer broadcasts the event to all listeners after delivery completes.

  • Save Main.java.

NotificationService not recognized?

  • Make sure you saved NotificationService.java from the previous substep. IntelliJ needs the file saved before other files can reference it.
  • Verify NotificationService.java is in the same com.patterns.notifications package as Main.java.

Help me fix unresolved references in Main.java

Add the pattern summary comment

A summary comment at the top of Main.java acts as a quick reference for anyone reading this code. It maps each pattern to the problem it solves.

  • In Main.java, scroll to the very top of the file.
  • Find this line:
package com.patterns.notifications;
  • Add the following summary comment block between the package declaration and the class declaration:
/*
 * Design Patterns Demonstrated:
 * 1. Builder    - Constructs complex Notification objects with optional fields
 * 2. Strategy   - Swaps delivery algorithms (Email, SMS, Push) at runtime
 * 3. Factory    - Creates the correct strategy without hardcoding types
 * 4. Decorator  - Adds logging and retry without modifying strategy code
 * 5. Observer   - Broadcasts delivery events to multiple independent listeners
 */

Why add this comment?

In production codebases, a top-level summary comment helps new developers understand the architecture at a glance. This maps each pattern to the specific problem it solves in your notification service.

  • Save Main.java.
  • Run the program by clicking the green play button next to the main method, or by pressing Ctrl+Shift+F10 (Windows) or Ctrl+Shift+R (macOS).
  • In the console output, you should see the individual pattern demos (Builder, Strategy, Factory, Decorator, Observer) followed by the === FULL SERVICE (ALL PATTERNS) === section showing three notifications sent with full logging, retry, and event broadcasting.

Program won't compile?

  • Check the Build output panel at the bottom of IntelliJ for the exact error line and message.
  • Make sure the facade demo code is placed inside the main() method's curly braces, not outside the class.
  • Verify the summary comment block is between the package declaration and the public class Main line, not inside the class body.

Help me fix my compilation error in the notification service

✔️ Awesome, I've got everything!

Great. Double check you have saved both NotificationService.java and Main.java.

ⓧ I'd like to double check the full code

Here is the complete NotificationService.java:

package com.patterns.notifications;

public class NotificationService {
    private final NotificationEventPublisher eventPublisher;

    public NotificationService() {
        this.eventPublisher = new NotificationEventPublisher();
        this.eventPublisher.subscribe(new AuditLogger());
        this.eventPublisher.subscribe(new AnalyticsTracker());
        this.eventPublisher.subscribe(new UserActivityUpdater());
    }

    public void send(String recipient, String message, String channel) {
        Notification notification = Notification.builder()
                .to(recipient)
                .withMessage(message)
                .viaChannel(channel)
                .build();

        DeliveryStrategy strategy = DeliveryStrategyFactory.getStrategy(channel);
        DeliveryStrategy decorated = new LoggingDecorator(new RetryDecorator(strategy, 3));

        System.out.println("\n--- Sending Notification ---");
        System.out.println("  " + notification);
        decorated.deliver(notification);

        System.out.println("  --- Event Listeners ---");
        eventPublisher.notifyListeners(notification);
        System.out.println("--- Done ---\n");
    }

    public NotificationEventPublisher getEventPublisher() {
        return eventPublisher;
    }
}

Here is the complete Main.java:

package com.patterns.notifications;

/*
 * Design Patterns Demonstrated:
 * 1. Builder    - Constructs complex Notification objects with optional fields
 * 2. Strategy   - Swaps delivery algorithms (Email, SMS, Push) at runtime
 * 3. Factory    - Creates the correct strategy without hardcoding types
 * 4. Decorator  - Adds logging and retry without modifying strategy code
 * 5. Observer   - Broadcasts delivery events to multiple independent listeners
 */
public class Main {

    public static void main(String[] args) {
        System.out.println("=== NOTIFICATION SERVICE - DESIGN PATTERNS DEMO ===\n");

        // --- Pattern 1: Builder ---
        System.out.println("--- BUILDER PATTERN ---");
        Notification emailNotification = Notification.builder()
                .to("alice@company.com")
                .withMessage("Your order has shipped!")
                .viaChannel("EMAIL")
                .withSubject("Order Update")
                .withPriority(2)
                .build();
        System.out.println("Built: " + emailNotification);

        Notification smsNotification = Notification.builder()
                .to("+1-555-0123")
                .withMessage("Your verification code is 482910")
                .viaChannel("SMS")
                .withPriority(1)
                .build();
        System.out.println("Built: " + smsNotification);

        Notification pushNotification = Notification.builder()
                .to("device-token-abc123")
                .withMessage("You have a new message from Bob")
                .viaChannel("PUSH")
                .withPriority(3)
                .build();
        System.out.println("Built: " + pushNotification);

        // --- Pattern 2: Strategy ---
        System.out.println("\n--- STRATEGY PATTERN ---");
        System.out.println("Delivering email with EmailDeliveryStrategy:");
        DeliveryStrategy emailStrategy = new EmailDeliveryStrategy();
        emailStrategy.deliver(emailNotification);

        System.out.println("\nDelivering SMS with SmsDeliveryStrategy:");
        DeliveryStrategy smsStrategy = new SmsDeliveryStrategy();
        smsStrategy.deliver(smsNotification);

        // --- Pattern 3: Factory ---
        System.out.println("\n--- FACTORY PATTERN ---");
        System.out.println("Getting strategy from factory for 'PUSH':");
        DeliveryStrategy factoryStrategy = DeliveryStrategyFactory.getStrategy("PUSH");
        factoryStrategy.deliver(pushNotification);

        // --- Pattern 4: Decorator ---
        System.out.println("\n--- DECORATOR PATTERN ---");
        System.out.println("Wrapping EmailStrategy with Logging and Retry:");
        DeliveryStrategy decorated = new LoggingDecorator(
                new RetryDecorator(new EmailDeliveryStrategy(), 3)
        );
        decorated.deliver(emailNotification);

        // --- Pattern 5: Observer ---
        System.out.println("\n--- OBSERVER PATTERN ---");
        NotificationEventPublisher publisher = new NotificationEventPublisher();
        publisher.subscribe(new AuditLogger());
        publisher.subscribe(new AnalyticsTracker());
        publisher.subscribe(new UserActivityUpdater());
        System.out.println("Broadcasting event to all listeners:");
        publisher.notifyListeners(emailNotification);

        // --- All Together: NotificationService Facade ---
        System.out.println("\n=== FULL SERVICE (ALL PATTERNS) ===");
        NotificationService service = new NotificationService();
        service.send("alice@company.com", "Welcome to our platform!", "EMAIL");
        service.send("+1-555-0456", "Your appointment is tomorrow at 2pm", "SMS");
        service.send("device-token-xyz789", "New friend request from Carol", "PUSH");

        System.out.println("=== DEMO COMPLETE ===");
    }
}

Your notification service is complete. Five patterns, working together, all hidden behind a three-parameter method. Next up, take on the Secret Mission and add a priority-based queue that processes urgent notifications first.

Secret mission

Add a Priority-Based Notification Queue

Your notification service handles messages one at a time. But in production, thousands of notifications flood in simultaneously. Urgent security alerts should not wait behind a weekly newsletter. Can you build a priority queue that always processes the most critical messages first?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The notification-service project folder and all its Java source files.
  • JDK 21 (Oracle) installed on your machine.
  • IntelliJ IDEA installed on your machine.

Keep everything running

No action needed. Choose this if you plan to keep building Java projects or want to revisit this code later.

  • Your notification-service project stays on disk and you can open it in IntelliJ IDEA anytime.
  • JDK 21 and IntelliJ IDEA remain installed and ready for your next Java project.

Pause - I'll come back to this later

Since everything is local, there is nothing to shut down. Your project files stay on disk until you are ready to return.

  • Close IntelliJ IDEA to free up memory.
  • Your project folder and installed tools remain in place for when you come back.

Delete - I don't want to use this again

Remove all project resources and tools to start fresh.

Delete the project folder:

macOS

  • Open Terminal and run this command to delete the project folder:
rm -rf ~/IdeaProjects/notification-service

If you saved the project somewhere else, replace the path with the actual location of your notification-service folder.

Windows

  • Open Command Prompt and run this command to delete the project folder:
rmdir /s /q %USERPROFILE%\IdeaProjects\notification-service

If you saved the project somewhere else, replace the path with the actual location of your notification-service folder.

Uninstall JDK 21:

macOS

  • Open Terminal and run this command to remove JDK 21:
sudo rm -rf /Library/Java/JavaVirtualMachines/jdk-21.jdk
  • Enter your administrator password when prompted.

Windows

  • Open Settings.
  • Select Apps, then Apps & features.
  • Search for Java(TM) SE Development Kit 21 in the list.
  • Click on it and select Uninstall.

Uninstall IntelliJ IDEA:

macOS

  • Open Finder and navigate to Applications.
  • Drag IntelliJ IDEA to the Trash.
  • Remove IntelliJ IDEA settings and caches by running these commands in Terminal:
rm -rf ~/Library/Application\ Support/JetBrains/
rm -rf ~/Library/Caches/JetBrains/

Windows

  • Open Settings.
  • Select Apps, then Apps & features.
  • Search for IntelliJ IDEA in the list.
  • Click on it and select Uninstall.

Nice Work!

Nice Work!

You just built a production-quality Notification Service that demonstrates five essential Java design patterns working together. Your service constructs, routes, decorates, and broadcasts notifications through a clean, extensible architecture.

Here's what you accomplished:

  • Built a multi-channel Notification Service that uses the Builder, Strategy, Factory, Decorator, and Observer patterns to dispatch messages via Email, SMS, and Push.
  • Refactored a naive if/else implementation into a clean, SOLID-compliant architecture where adding new channels or behaviors requires zero changes to existing code.
  • Stacked decorators for logging and retry around delivery strategies, and wired an event publisher that broadcasts to multiple independent listeners after each delivery.
  • Added a priority-based notification queue that processes urgent messages first, introducing queue processing and concurrency concepts.

Ready to quiz yourself?