AlgoDaily

Design Patterns in Java

Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.

Why Patterns Matter

From project skeleton to project design

In Project Setup and Version Control, you created a clean baseline: a runnable Java project, a Git working loop, and repo hygiene. Now we begin shaping code so it can survive real project change.

Design patterns are not magic templates. They are shared names for proven object-oriented solutions.

In real-time Java projects, patterns help you answer questions like:

  • Where should object creation live?
  • How do we swap behavior without rewriting business logic?
  • How do modules communicate without becoming tightly coupled?
  • How do we keep classes small enough to test and evolve?

We will focus on patterns you will reuse in later projects, especially before we build services, persistence layers, chat features, and e-commerce flows.

Why Patterns Matter

Pattern Thinking

Patterns solve recurring pressure

A design pattern usually appears when code starts feeling one of these pressures:

Pressure Typical symptom Helpful pattern family Too many if/else branches Logic changes often Behavioral patterns Object creation is scattered Constructors everywhere Creational patterns Classes know too much Hard to replace pieces Structural patterns Modules need notifications Direct calls multiply Behavioral patterns
Three useful families
  • Creational: how objects are created. Example: Factory Method.
  • Structural: how classes and objects are composed. Example: Adapter, Facade.
  • Behavioral: how objects communicate and delegate behavior. Example: Strategy, Observer.
Real-project rule

Use a pattern when it makes code simpler under change. Do not add a pattern just to sound architectural.

Pattern Thinking

Strategy Pattern

Swap behavior without rewriting the caller

The Strategy pattern lets you define a family of algorithms, place each one in its own class, and make them interchangeable.

This is useful when a project has behavior that varies:

  • discount calculation
  • shipping cost calculation
  • authentication method
  • ranking or scoring logic
  • notification channel selection

Instead of writing one large method full of conditional branches, the caller depends on a small interface.

Mental model
CheckoutService ---> DiscountStrategy
                         /     |      \
                  NoDiscount  Festival  Member

The service does not care which discount is used. It only calls apply.

```java
public class Main {
    interface DiscountStrategy {
        double apply(double subtotal);
    }

    static class NoDiscount implements DiscountStrategy {
        public double apply(double subtotal) {
            return subtotal;
        }
    }

    static class FestivalDiscount implements DiscountStrategy {
        public double apply(double subtotal) {
            return subtotal * 0.80;
        }
    }

    static class CheckoutService {
        private final DiscountStrategy discountStrategy;

        CheckoutService(DiscountStrategy discountStrategy) {
            this.discountStrategy = discountStrategy;
        }

        double finalAmount(double subtotal) {
            return discountStrategy.apply(subtotal);
        }
    }

    public static void main(String[] args) {
        CheckoutService normalCheckout = new CheckoutService(new NoDiscount());
        CheckoutService festivalCheckout = new CheckoutService(new FestivalDiscount());

        System.out.println("Normal: " + normalCheckout.finalAmount(1000));
        System.out.println("Festival: " + festivalCheckout.finalAmount(1000));
    }
}
```

Choose The Pattern

A checkout module needs to support multiple discount algorithms. The selected algorithm may change depending on campaign, user type, or region.

Which pattern best fits this need?

Factory Method

Move creation decisions out of business logic

The Factory Method pattern centralizes object creation behind a method or class.

This helps when code knows what kind of object it needs, but should not know all construction details.

Common real-project examples:

  • create a report exporter based on pdf, csv, or json
  • create a notification sender based on channel
  • create a parser based on file type
  • create a payment handler based on provider

The goal is not to hide every constructor. The goal is to prevent object creation rules from leaking across the project.

```java
public class Main {
    interface ReportExporter {
        String export(String reportTitle);
    }

    static class PdfExporter implements ReportExporter {
        public String export(String reportTitle) {
            return "PDF export created for: " + reportTitle;
        }
    }

    static class CsvExporter implements ReportExporter {
        public String export(String reportTitle) {
            return "CSV export created for: " + reportTitle;
        }
    }

    static class ExporterFactory {
        static ReportExporter create(String type) {
            if ("pdf".equalsIgnoreCase(type)) {
                return new PdfExporter();
            }
            if ("csv".equalsIgnoreCase(type)) {
                return new CsvExporter();
            }
            throw new IllegalArgumentException("Unsupported export type: " + type);
        }
    }

    public static void main(String[] args) {
        ReportExporter exporter = ExporterFactory.create("pdf");
        System.out.println(exporter.export("Monthly Sales"));

        ReportExporter csvExporter = ExporterFactory.create("csv");
        System.out.println(csvExporter.export("Inventory"));
    }
}
```

Factory Check

True or false: A factory is most useful when object creation rules are starting to spread across many unrelated classes.

Singleton Caution

One instance is not always good design

The Singleton pattern ensures a class has only one instance and provides a global access point.

It can be useful for truly shared, process-wide resources, such as:

  • application configuration loaded once
  • a registry of immutable metadata
  • a shared clock abstraction in a small app

But in real Java projects, Singleton is often overused.

Why be careful?

Singletons can introduce hidden dependencies:

Class A ---> GlobalConfig.getInstance()
Class B ---> GlobalConfig.getInstance()
Class C ---> GlobalConfig.getInstance()

This makes testing harder because classes silently reach into global state.

Better project habit

Prefer passing dependencies explicitly through constructors. This keeps classes easier to test, replace, and reason about. Later, frameworks can automate dependency wiring, but the design principle is the same: dependencies should be visible.

Singleton Caution

Name The Pattern

A payment module chooses between CreditCardPayment, WalletPayment, and BankTransferPayment at runtime. Each option implements the same payment behavior interface.

The best pattern name is: _____

Observer Pattern

Notify interested modules without hardwiring them

The Observer pattern lets one object publish an event while other objects subscribe to react.

This is useful when one action should trigger multiple side effects:

  • order placed → send email, update inventory, create audit log
  • user registered → send welcome message, create profile, notify analytics
  • file uploaded → scan file, generate thumbnail, log activity

The publisher should not need to know every concrete class that reacts. It only notifies listeners through an interface.

This style will become very useful when we later build real-time and service-oriented features.

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

public class Main {
    static class Order {
        final String id;
        final double total;

        Order(String id, double total) {
            this.id = id;
            this.total = total;
        }
    }

    interface OrderListener {
        void onOrderPlaced(Order order);
    }

    static class EmailNotifier implements OrderListener {
        public void onOrderPlaced(Order order) {
            System.out.println("Email sent for order " + order.id);
        }
    }

    static class InventoryUpdater implements OrderListener {
        public void onOrderPlaced(Order order) {
            System.out.println("Inventory reserved for order " + order.id);
        }
    }

    static class OrderService {
        private final List<OrderListener> listeners = new ArrayList<>();

        void addListener(OrderListener listener) {
            listeners.add(listener);
        }

        void placeOrder(Order order) {
            System.out.println("Order placed: " + order.id + ", total=" + order.total);
            for (OrderListener listener : listeners) {
                listener.onOrderPlaced(order);
            }
        }
    }

    public static void main(String[] args) {
        OrderService orderService = new OrderService();
        orderService.addListener(new EmailNotifier());
        orderService.addListener(new InventoryUpdater());

        orderService.placeOrder(new Order("ORD-101", 2499.0));
    }
}
```

Project Pattern Map

How to use patterns in your course projects

As you continue through this Java project course, treat patterns as design tools, not decorations.

Practical selection guide
If you notice... Consider... many interchangeable algorithms Strategy object creation logic repeated Factory Method global instance temptation explicit dependencies before Singleton one action causing many reactions Observer
A real-project workflow
  1. Start with simple code.
  2. Notice repeated change pressure.
  3. Extract an interface around the variation.
  4. Move implementations into focused classes.
  5. Keep the caller dependent on abstractions.
  6. Commit the refactor cleanly using the Git habits from Project Setup and Version Control.

Next, when we build a RESTful web service, these patterns will help keep controllers, services, and supporting classes from collapsing into one large procedural block.

Project Pattern Map

Open interactive view →