Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›java›Design Patterns in Java

Java Development Tools and Practices

Design Patterns in Java

Design patterns are proven, reusable solutions to recurring architectural problems encountered during software development. They provide a shared vocabulary and established blueprints that improve code maintainability, scalability, and clarity. Developers should utilize these patterns when complex logic requires structure to avoid tight coupling or redundant code implementation.

Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global access point to that object. In Java, this is achieved by making the constructor private, creating a static instance of the class, and providing a public static getter method. This is essential for resource management, such as configuration managers or database connection pools where multiple instances would lead to inconsistent states or memory overhead. By controlling the instantiation, the system prevents unauthorized duplication. When designing systems, use this pattern sparingly as it introduces global state, which can make testing more difficult. However, its benefit in scenarios like logging, where a single central point of entry is required to synchronize writes to a file, is unparalleled. You must ensure thread safety by using synchronized access or static initialization to prevent race conditions during the initial creation of the instance by multiple threads simultaneously.

public class ConfigurationManager {
    // Static instance created during class loading
    private static final ConfigurationManager INSTANCE = new ConfigurationManager();

    private ConfigurationManager() {}

    public static ConfigurationManager getInstance() {
        return INSTANCE;
    }
}

Factory Method Pattern

The Factory Method pattern defines an interface for creating objects but delegates the specific instantiation process to subclasses. This promotes loose coupling because the client code interacts with an abstraction rather than concrete classes. By isolating object creation, you make the application more resilient to changes; if the underlying implementation of a product changes, you only modify the factory, not the high-level business logic. This is particularly useful in framework development or when an application needs to support multiple variants of a service, such as different types of document exporters. The reasoning here is based on the Dependency Inversion Principle, which encourages relying on interfaces. When you implement this, consider that it adds an extra layer of complexity, but the trade-off is a flexible design where adding a new type simply involves creating a new subclass and a corresponding factory method, adhering to the Open/Closed Principle.

public interface PaymentProcessor { void process(double amount); }

public class CreditCardProcessor implements PaymentProcessor {
    public void process(double amount) { /* Logic for card */ }
}

public class PaymentFactory {
    public static PaymentProcessor getProcessor(String type) {
        if ("card".equals(type)) return new CreditCardProcessor();
        return null;
    }
}

Builder Pattern

The Builder pattern is designed to solve the problem of constructors with too many parameters, often called the 'telescoping constructor' antipattern. When an object requires several optional fields, creating multiple constructor variants leads to confusing and error-prone code. A Builder provides a fluent API, allowing the developer to chain method calls to set specific parameters before calling a final build method. This separation of concerns ensures that the object is always created in a valid, immutable state. It is highly beneficial for complex data objects that require non-trivial validation logic during initialization. By separating the construction process from the representation, you can construct different complex objects using the same building process. This approach is superior because it makes the calling code readable and prevents the creation of partially initialized objects that could lead to runtime exceptions if fields were left at their default null values.

public class User {
    private final String name; // immutable
    private User(Builder b) { this.name = b.name; }
    public static class Builder {
        private String name;
        public Builder name(String n) { this.name = n; return this; }
        public User build() { return new User(this); }
    }
}

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. This is a behavioral pattern that enables loose coupling between the subject and its observers, allowing them to vary independently. The subject does not need to know the specific types of the observers, only that they implement a specific notification interface. This is the foundation of event-driven architectures in Java, such as GUI listeners or messaging systems. By using this pattern, you avoid hard-coding dependencies between systems that need to communicate updates, thus making your codebase much easier to extend. If a new observer needs to monitor the subject, it simply registers itself without modifying the subject's code. This modularity ensures that the system remains maintainable even as new requirements regarding real-time synchronization or status tracking are introduced over time.

import java.util.*;
public class NewsAgency {
    private List<Observer> observers = new ArrayList<>();
    public void addObserver(Observer o) { observers.add(o); }
    public void notifyObservers(String msg) {
        for(Observer o : observers) o.update(msg);
    }
}

Strategy Pattern

The Strategy pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. Instead of using complex conditional statements like 'if-else' or 'switch' to determine behavior, you delegate the execution to a strategy object. This design strictly follows the Open/Closed Principle because you can introduce new strategies without modifying the client code that executes them. It is useful in any scenario involving alternative logic paths, such as different tax calculation rules, sorting algorithms, or file compression methods. By treating algorithms as objects, you improve testability, as each strategy can be verified in isolation. The reasoning is to favor composition over inheritance; by holding a reference to a strategy interface, the context object can switch its behavior dynamically based on user input or state. This results in cleaner, more modular code that avoids the bloat of massive, multi-purpose classes that are difficult to debug or maintain.

public interface SortStrategy { void sort(int[] arr); }

public class QuickSort implements SortStrategy {
    public void sort(int[] arr) { /* Logic here */ }
}

public class Sorter {
    private SortStrategy strategy;
    public void setStrategy(SortStrategy s) { this.strategy = s; }
    public void execute(int[] arr) { strategy.sort(arr); }
}

Key points

  • Design patterns provide standardized solutions to common software engineering challenges.
  • The Singleton pattern restricts class instantiation to a single shared global instance.
  • Factory methods simplify object creation by abstracting the concrete implementation details.
  • The Builder pattern solves complexity issues associated with objects requiring many optional parameters.
  • Observer patterns enable efficient communication between subjects and multiple dependent objects.
  • Strategy patterns allow for interchangeable behavior through the use of encapsulated algorithms.
  • Effective use of patterns promotes loose coupling and adheres to the Open/Closed Principle.
  • Choosing the right design pattern is essential for writing scalable and maintainable Java applications.

Common mistakes

  • Mistake: Implementing the Singleton pattern with public constructors. Why it's wrong: It allows external classes to instantiate the class multiple times, violating the pattern's intent. Fix: Use a private constructor to restrict instantiation to within the class.
  • Mistake: Overusing the Strategy pattern for simple conditional logic. Why it's wrong: It leads to 'class explosion,' making the codebase unnecessarily complex for trivial tasks. Fix: Only use Strategy when you have three or more interchangeable algorithms that change frequently.
  • Mistake: Failing to define an interface for the Proxy pattern. Why it's wrong: The client cannot treat the proxy and the real object interchangeably, defeating the purpose of the pattern. Fix: Ensure both the proxy and the subject implement the same common interface.
  • Mistake: Holding references to heavy objects in the Observer pattern. Why it's wrong: It causes memory leaks because the 'subject' keeps the 'observer' alive even when it's no longer needed. Fix: Use WeakReferences or ensure a proper 'unsubscribe' mechanism is implemented.
  • Mistake: Creating Factory classes that use switch statements based on strings. Why it's wrong: It violates the Open/Closed Principle because you must modify the factory code whenever a new type is added. Fix: Use dependency injection or a registry map to map types to object creators.

Interview questions

What is the Singleton design pattern in Java, and how do you implement it correctly in a multi-threaded environment?

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. In a multi-threaded environment, a simple implementation can lead to race conditions. To implement it correctly, you should use the 'Initialization-on-demand holder' idiom or use a private static volatile instance with double-checked locking. By declaring the instance as volatile, you ensure that multiple threads handle the singleton instance correctly when it is being initialized by the thread that first accesses it, preventing memory visibility issues.

Explain the purpose of the Factory Method pattern and why we use it in Java applications.

The Factory Method pattern defines an interface for creating an object but lets subclasses decide which class to instantiate. We use this in Java to promote loose coupling by eliminating the need to bind application-specific classes into the code. Instead of using the 'new' keyword directly to instantiate objects, the client code calls the factory method. This makes the code easier to maintain, as you can introduce new concrete types without modifying the existing client code, adhering to the Open/Closed Principle.

How does the Observer pattern work in Java, and where is it commonly applied?

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. In Java, this is often implemented using the PropertyChangeListener interface or the built-in Observer/Observable classes, though the latter is now deprecated. It is commonly applied in UI frameworks, such as Swing or JavaFX, where event listeners are attached to components. This pattern is essential for creating reactive systems where decoupled components need to stay synchronized with data changes.

Compare the Decorator pattern with the Inheritance approach for extending functionality in Java.

Inheritance provides a static, compile-time way to extend functionality by creating subclasses, which can lead to a 'class explosion' if you need many combinations of features. In contrast, the Decorator pattern offers a dynamic, runtime approach by wrapping objects. For example, in Java I/O, you can wrap a 'FileInputStream' with 'BufferedInputStream' to add functionality without modifying the original class. The Decorator pattern is superior when you need to combine behaviors flexibly at runtime without creating a deep, rigid hierarchy of subclasses.

What is the Strategy pattern, and how does it utilize Java interfaces to achieve its goals?

The Strategy pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. In Java, this is achieved by creating an interface that defines the algorithm's signature, and then implementing that interface in various concrete classes. The context class holds a reference to the interface rather than a concrete implementation. This allows the client to switch algorithms dynamically at runtime by swapping the concrete implementation. This approach is highly effective for removing complex conditional logic like long if-else or switch statements.

Explain the concept of the Proxy pattern in Java and describe a practical use case, such as Virtual Proxy or Protection Proxy.

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. A practical use case is the Virtual Proxy, used for lazy initialization of resource-intensive objects. If you have a large image or a heavy database connection, you create a proxy object that represents the real object but only initializes it when a method is actually called. This improves startup performance significantly. Additionally, the java.lang.reflect.Proxy API allows developers to create dynamic proxies, which are foundational to many frameworks like Spring for implementing AOP, transaction management, and logging, as they can intercept method calls transparently.

All java interview questions →

Check yourself

1. You need to add new behavior to an object at runtime without changing its class structure. Which pattern is most appropriate?

  • A.Singleton
  • B.Decorator
  • C.Strategy
  • D.Facade
Show answer

B. Decorator
The Decorator pattern is designed to add responsibilities to individual objects dynamically. Strategy changes the algorithm, Singleton restricts instantiation, and Facade simplifies an interface.

2. A system requires a single point of access to a resource-intensive object. Why should you avoid a simple static global variable for this?

  • A.Static variables are not thread-safe.
  • B.They prevent lazy initialization and make unit testing difficult.
  • C.They violate the Liskov Substitution Principle.
  • D.They prevent the use of interfaces.
Show answer

B. They prevent lazy initialization and make unit testing difficult.
Static variables initialize when the class loads, preventing lazy loading. They also create hidden dependencies that make it harder to mock objects during testing. Thread safety is a separate issue, and the others are not direct impacts of using static globals.

3. In the Factory Method pattern, what is the primary benefit of returning an interface type rather than a concrete class type?

  • A.It improves performance by reducing object size.
  • B.It hides the implementation details, allowing the caller to rely on abstractions rather than specifics.
  • C.It allows the factory to use reflection automatically.
  • D.It ensures the object is always instantiated as a singleton.
Show answer

B. It hides the implementation details, allowing the caller to rely on abstractions rather than specifics.
Programming to an interface reduces coupling, allowing the concrete implementation to change without impacting the client. Performance, reflection, and singletons are not the primary drivers for this design choice.

4. Why is it recommended to use a static inner class (Initialization-on-demand holder idiom) for implementing a Singleton in Java?

  • A.It requires less code than synchronized methods.
  • B.It provides thread-safe lazy initialization without needing explicit synchronization.
  • C.It allows the class to be serializable by default.
  • D.It allows the singleton to be extended by other classes.
Show answer

B. It provides thread-safe lazy initialization without needing explicit synchronization.
The class loader handles the static inner class initialization only when it's referenced, ensuring thread safety while maintaining lazy loading without the performance hit of a synchronized block. The other options are either incorrect or unrelated.

5. An application has a complex subsystem with many classes. You want to provide a simplified interface for client code. Which pattern should you use?

  • A.Adapter
  • B.Proxy
  • C.Facade
  • D.Bridge
Show answer

C. Facade
Facade provides a unified, higher-level interface to a set of interfaces in a subsystem. Adapter connects incompatible interfaces, Proxy controls access, and Bridge decouples abstraction from implementation.

Take the full java quiz →

← PreviousLogging Frameworks (Log4j, SLF4J)Next →Java Memory Management and Garbage Collection

java

37 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app