Common Design Patterns in Java (Overview)

Design patterns are reusable solutions to recurring design problems. This article surveys patterns commonly used in backend development: Singleton, Factory / Abstract Factory, Builder, Strategy, Template Method, Observer, Proxy, and Decorator, with Java examples and a quick reference for when to use each.

Overview

  • Creational: Singleton (single instance), Factory/Abstract Factory (encapsulate creation), Builder (objects with many optional parameters).
  • Behavioral: Strategy (swappable algorithms), Template Method (fixed skeleton, customizable steps), Observer (event notification).
  • Structural: Proxy (access control, lazy loading, remote), Decorator (transparently add responsibilities).
  • Principle: Prefer simple composition and interfaces; use patterns when they clearly help. Do not apply patterns for their own sake (YAGNI, KISS).

Example

Example 1: Singleton (lazy + double-check, illustrative only)

Java
public class Config {
    private static volatile Config instance;
    public static Config getInstance() {
        if (instance == null) {
            synchronized (Config.class) {
                if (instance == null) instance = new Config();
            }
        }
        return instance;
    }
}
  • Use for: global config, connection pool, thread pool—anything that should have a single instance. Watch for thread safety, testability, and extensibility. Prefer dependency injection over singletons when possible.

Example 2: Strategy — swappable algorithm

Java
public interface PayStrategy { void pay(Order o); }
public class AlipayStrategy implements PayStrategy { ... }
public class WechatPayStrategy implements PayStrategy { ... }

public class OrderService {
    private PayStrategy strategy;
    public void setStrategy(PayStrategy s) { this.strategy = s; }
    public void checkout(Order o) { strategy.pay(o); }
}
  • Use for: multiple payment methods, validation rules, sorting or recommendation algorithms—any “same interface, multiple swappable implementations.”

Example 3: Builder — many parameters, optional fields

Java
public class Request {
    public static Builder builder() { return new Builder(); }
    public static class Builder {
        private String url; private String method = "GET"; private int timeout = 5000;
        public Builder url(String u) { url = u; return this; }
        public Builder method(String m) { method = m; return this; }
        public Builder timeout(int t) { timeout = t; return this; }
        public Request build() { return new Request(this); }
    }
}
Request r = Request.builder().url("/api").timeout(3000).build();
  • Use for: config objects, DTOs, HTTP requests—any object with many parameters and optional fields. Improves readability and avoids huge constructors.

Example 4: Template Method

Java
public abstract class AbstractHandler {
    public final void handle(Request req) {
        validate(req);
        doHandle(req);
        afterHandle(req);
    }
    protected abstract void doHandle(Request req);
}
  • Use for: fixed flow (e.g. auth → business → audit) where subclasses only implement the varying steps.

Example 5: Observer (event notification)

Java
public interface OrderListener {
    void onOrderCreated(Order o);
}
public class OrderService {
    private List<OrderListener> listeners = new ArrayList<>();
    public void addListener(OrderListener l) { listeners.add(l); }
    public void createOrder(Order o) {
        // ... create ...
        listeners.forEach(l -> l.onOrderCreated(o));
    }
}
  • Use for: event-driven logic, pub-sub style updates. Modern Java often uses reactive streams or message queues instead.

Example 6: Proxy (remote, lazy, or access control)

Java
// Remote: RPC stubs are proxies for remote objects
// Lazy: Proxy defers expensive init until first use
public class LazyService {
    private HeavyService heavy;
    public void doWork() {
        if (heavy == null) heavy = new HeavyService();
        heavy.doWork();
    }
}
  • Use for: remote calls, lazy loading, logging, access control. Spring AOP and dynamic proxies implement many of these.

Example 7: Decorator (add behavior transparently)

Java
// InputStream decorators: BufferedInputStream, GZIPInputStream
public class BufferedInputStream extends FilterInputStream {
    // Wraps another InputStream, adds buffering
}
  • Use when you want to add behavior without changing the original class. Prefer composition over inheritance.

Example 8: Pattern quick reference

PatternTypeTypical use
SingletonCreationalConfig, connection pool, thread pool
Factory / Abstract FactoryCreationalCreate implementations by type or config
BuilderCreationalObjects with many optional parameters
StrategyBehavioralSwappable algorithms or rules
Template MethodBehavioralFixed flow, customizable steps
ObserverBehavioralEvent-driven, pub-sub
ProxyStructuralRemote, lazy load, access control
DecoratorStructuralTransparently add behavior (e.g. buffered streams)

Example 9: Factory pattern

Java
public interface Cache { ... }
public class RedisCache implements Cache { ... }
public class LocalCache implements Cache { ... }

public class CacheFactory {
    public static Cache create(String type) {
        return switch (type) {
            case "redis" -> new RedisCache();
            case "local" -> new LocalCache();
            default -> throw new IllegalArgumentException(type);
        };
    }
}
  • Encapsulates object creation; callers depend on the interface, not concrete types. Abstract Factory extends this when you have families of related products.

Core Mechanism / Behavior

  • Singleton: Controls instance count and access; watch threading and testability. Prefer DI when possible.
  • Strategy: Injects implementation via interface; algorithm can be swapped at runtime; supports open-closed principle.
  • Template Method: Parent defines skeleton; child implements hooks; reduces duplicate flow code.
  • Builder: Chain setters, validate and construct in build(); supports immutable or fully initialized objects.
  • Observer: Subject notifies observers; decouples producers and consumers; consider memory leaks (unregister when done).

When Not to Use Patterns

  • Singleton: Prefer dependency injection; singletons make testing and mocking harder.
  • Abstract Factory: Overkill when you have only one or two implementations.
  • Builder: For objects with 2–3 parameters, a constructor or factory method is simpler.
  • Template Method: Consider composition and strategy instead of inheritance when the steps vary a lot.

Anti-Patterns to Avoid

  • God Object: One class doing too much; split by responsibility.
  • Anemic Domain Model: Data classes with no behavior; put logic where it belongs.
  • Pattern overload: Using many patterns in a small feature; keep it simple.

Key Rules

  • Satisfy requirements and readability first; introduce patterns when they clearly help. Avoid over-engineering.
  • Prefer interfaces and composition for testability and swappable implementations (Strategy, Factory, Proxy all rely on this).
  • Use Singleton sparingly: global state hurts testing and extension; prefer dependency injection or context.
  • Know when not to use a pattern: e.g. avoid Singleton for test doubles; avoid excessive abstraction for a single implementation.

What's Next

See Spring IOC and Bean lifecycle for factory and singleton scopes. See Spring AOP for proxy and decorator patterns.