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)
Javapublic 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
Javapublic 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
Javapublic 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
Javapublic 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)
Javapublic 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
| Pattern | Type | Typical use |
|---|---|---|
| Singleton | Creational | Config, connection pool, thread pool |
| Factory / Abstract Factory | Creational | Create implementations by type or config |
| Builder | Creational | Objects with many optional parameters |
| Strategy | Behavioral | Swappable algorithms or rules |
| Template Method | Behavioral | Fixed flow, customizable steps |
| Observer | Behavioral | Event-driven, pub-sub |
| Proxy | Structural | Remote, lazy load, access control |
| Decorator | Structural | Transparently add behavior (e.g. buffered streams) |
Example 9: Factory pattern
Javapublic 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.