Proven solutions to recurring problems — and when NOT to use them
Object creation mechanisms. Singleton, Factory, Builder, Prototype.
Object composition. Adapter, Decorator, Facade, Proxy, Composite.
Object communication. Strategy, Observer, Command, Template Method.
Patterns are tools, not goals. The worst codebases are over-engineered with patterns used for their own sake. Apply a pattern when you have the problem it solves, not preemptively.
Problem: Need exactly one instance of a class (database connection pool, configuration, logger).
# Python: module-level is effectively singleton # config.py class Config: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._load_config() return cls._instance def _load_config(self): self.db_url = "postgresql://..." # Or just use a module (simpler) # config.py DB_URL = "postgresql://..." # Importing the module gives same instance
// Classic double-checked locking (thread-safe) public class Config { private static volatile Config instance; private final String dbUrl; private Config() { // Private constructor - can't instantiate from outside this.dbUrl = loadFromFile(); } public static Config getInstance() { if (instance == null) { synchronized (Config.class) { if (instance == null) { instance = new Config(); } } } return instance; } public String getDbUrl() { return dbUrl; } } // Usage Config config = Config.getInstance(); config.getDbUrl();
// RECOMMENDED: Enum singleton (thread-safe, serialization-safe) public enum Config { INSTANCE; private final String dbUrl; Config() { this.dbUrl = loadFromFile(); } public String getDbUrl() { return dbUrl; } private String loadFromFile() { return "postgresql://..."; } } // Usage String url = Config.INSTANCE.getDbUrl(); // Why enum? // - JVM guarantees single instance // - Thread-safe by default // - Serialization works correctly // - Reflection can't create new instances // EVEN BETTER: Use Dependency Injection instead! @Component public class Config { // Spring manages singleton lifecycle }
Singletons are essentially global state — they make testing hard and hide dependencies. Prefer Dependency Injection (Spring, Guice) which gives you singleton behavior with testability and explicit dependencies.
Problem: Need to create objects without specifying exact class, or creation logic is complex.
// Simple Factory: static method that creates objects public class NotificationFactory { public static Notification create(String type) { return switch (type) { case "email" -> new EmailNotification(); case "sms" -> new SmsNotification(); case "push" -> new PushNotification(); default -> throw new IllegalArgumentException("Unknown: " + type); }; } } // Usage Notification notif = NotificationFactory.create("sms"); notif.send("Hello!"); // Real-world Java examples: List<String> list = List.of("a", "b"); // Factory method Optional<String> opt = Optional.of("x"); // Factory method LocalDate date = LocalDate.now(); // Factory method ExecutorService exec = Executors.newFixedThreadPool(4); // Factory
// Factory Method: subclasses decide which class to instantiate public abstract class Document { public abstract void open(); // Factory method - subclasses override public abstract Page createPage(); public void addPage() { Page page = createPage(); // Calls subclass implementation pages.add(page); } } public class WordDocument extends Document { @Override public Page createPage() { return new WordPage(); // Word-specific page } } public class PdfDocument extends Document { @Override public Page createPage() { return new PdfPage(); // PDF-specific page } } // Client code works with abstract Document Document doc = getDocument(); // Could be Word or PDF doc.addPage(); // Creates correct page type automatically
// Abstract Factory: create families of related objects public interface UIFactory { Button createButton(); TextField createTextField(); Checkbox createCheckbox(); } public class MaterialUIFactory implements UIFactory { public Button createButton() { return new MaterialButton(); } public TextField createTextField() { return new MaterialTextField(); } public Checkbox createCheckbox() { return new MaterialCheckbox(); } } public class IOSUIFactory implements UIFactory { public Button createButton() { return new IOSButton(); } public TextField createTextField() { return new IOSTextField(); } public Checkbox createCheckbox() { return new IOSCheckbox(); } } // Usage: entire UI is consistent public class Application { private final UIFactory factory; public Application(UIFactory factory) { this.factory = factory; // Inject the factory } public void createUI() { Button btn = factory.createButton(); TextField field = factory.createTextField(); // All components match the same style } }
Problem: Object has many optional parameters, or construction requires multiple steps.
# Python: keyword arguments handle this naturally class HttpRequest: def __init__(self, url, method="GET", headers=None, body=None, timeout=30): self.url = url self.method = method self.headers = headers or {} self.body = body self.timeout = timeout # Named parameters = readable req = HttpRequest( url="https://api.example.com", method="POST", headers={"Content-Type": "application/json"}, timeout=60 ) # Python doesn't really need the Builder pattern
// Java: Builder pattern provides named parameters + validation public class HttpRequest { private final String url; // Required private final String method; // Optional, default GET private final Map<String, String> headers; private final String body; private final Duration timeout; private HttpRequest(Builder builder) { this.url = builder.url; this.method = builder.method; this.headers = Map.copyOf(builder.headers); this.body = builder.body; this.timeout = builder.timeout; } public static Builder builder(String url) { return new Builder(url); } // Getters... public static class Builder { private final String url; // Required - in constructor private String method = "GET"; private Map<String, String> headers = new HashMap<>(); private String body; private Duration timeout = Duration.ofSeconds(30); private Builder(String url) { this.url = Objects.requireNonNull(url); } public Builder method(String method) { this.method = method; return this; } public Builder header(String key, String value) { this.headers.put(key, value); return this; } public Builder body(String body) { this.body = body; return this; } public Builder timeout(Duration timeout) { this.timeout = timeout; return this; } public HttpRequest build() { // Validation here if (method.equals("POST") && body == null) { throw new IllegalStateException("POST requires body"); } return new HttpRequest(this); } } } // Usage - fluent, readable, validated HttpRequest request = HttpRequest.builder("https://api.example.com") .method("POST") .header("Content-Type", "application/json") .header("Authorization", "Bearer token") .body("{\"key\": \"value\"}") .timeout(Duration.ofSeconds(60)) .build();
Lombok generates builders automatically: @Builder
Problem: You have an existing class, but its interface doesn't match what you need.
// You need this interface public interface PaymentProcessor { PaymentResult charge(Money amount, Card card); } // But you have this legacy/third-party class public class LegacyPaymentGateway { public int processPayment(double amountCents, String cardNumber, String expiry, String cvv) { // Returns status code } } // Adapter makes legacy class work with new interface public class LegacyPaymentAdapter implements PaymentProcessor { private final LegacyPaymentGateway gateway; public LegacyPaymentAdapter(LegacyPaymentGateway gateway) { this.gateway = gateway; } @Override public PaymentResult charge(Money amount, Card card) { // Translate between interfaces double cents = amount.toCents(); int statusCode = gateway.processPayment( cents, card.getNumber(), card.getExpiry(), card.getCvv() ); // Translate result return switch (statusCode) { case 0 -> PaymentResult.success(); case 1 -> PaymentResult.declined(); default -> PaymentResult.error("Unknown status: " + statusCode); }; } } // Usage - your code uses the clean interface PaymentProcessor processor = new LegacyPaymentAdapter(legacyGateway); PaymentResult result = processor.charge(money, card);
Problem: Need to add responsibilities to objects without modifying their class.
# Python has built-in decorator syntax def log_calls(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") result = func(*args, **kwargs) print(f"Returned: {result}") return result return wrapper @log_calls def add(a, b): return a + b # Class-based decorator class CachingDecorator: def __init__(self, service): self.service = service self.cache = {} def get(self, key): if key not in self.cache: self.cache[key] = self.service.get(key) return self.cache[key]
// Base interface public interface DataSource { String read(); void write(String data); } // Concrete implementation public class FileDataSource implements DataSource { private final String filename; public String read() { /* read from file */ } public void write(String data) { /* write to file */ } } // Base decorator - wraps a DataSource public abstract class DataSourceDecorator implements DataSource { protected final DataSource wrapped; public DataSourceDecorator(DataSource source) { this.wrapped = source; } } // Encryption decorator public class EncryptionDecorator extends DataSourceDecorator { public EncryptionDecorator(DataSource source) { super(source); } @Override public String read() { return decrypt(wrapped.read()); // Add behavior } @Override public void write(String data) { wrapped.write(encrypt(data)); // Add behavior } } // Compression decorator public class CompressionDecorator extends DataSourceDecorator { @Override public String read() { return decompress(wrapped.read()); } @Override public void write(String data) { wrapped.write(compress(data)); } } // Usage - decorators are stackable! DataSource source = new FileDataSource("data.txt"); source = new CompressionDecorator(source); // Add compression source = new EncryptionDecorator(source); // Add encryption source.write("secret data"); // Compressed, then encrypted, then written source.read(); // Read, decrypted, decompressed // Real-world: Java I/O streams are decorators! new BufferedInputStream( new GZIPInputStream( new FileInputStream("data.gz") ) );
Problem: Need to control access, add lazy loading, caching, or logging.
// Interface public interface Image { void display(); } // Real implementation (expensive to create) public class HighResImage implements Image { private final byte[] data; public HighResImage(String path) { this.data = loadFromDisk(path); // Expensive! } public void display() { renderToScreen(data); } } // LAZY LOADING PROXY public class LazyImageProxy implements Image { private final String path; private HighResImage realImage; // Loaded on demand public LazyImageProxy(String path) { this.path = path; // Just store path, don't load yet } public void display() { if (realImage == null) { realImage = new HighResImage(path); // Load now } realImage.display(); } } // CACHING PROXY public class CachingUserService implements UserService { private final UserService delegate; private final Cache<String, User> cache; @Override public User getUser(String id) { return cache.computeIfAbsent(id, delegate::getUser); } } // LOGGING PROXY (via Java dynamic proxy) UserService loggingProxy = (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class[]{UserService.class}, (proxy, method, args) -> { log.info("Calling: {}", method.getName()); Object result = method.invoke(realService, args); log.info("Returned: {}", result); return result; } ); // Spring uses proxies extensively for: // - @Transactional (wrap method in transaction) // - @Cacheable (cache method results) // - @Async (run method in separate thread)
Problem: Need to switch between different algorithms or behaviors at runtime.
# Python: just pass functions (first-class functions) def sort_by_name(users): return sorted(users, key=lambda u: u.name) def sort_by_age(users): return sorted(users, key=lambda u: u.age) def process_users(users, sort_strategy): sorted_users = sort_strategy(users) # ... process_users(users, sort_by_name) process_users(users, sort_by_age) # Python doesn't need Strategy classes - functions are enough
// Strategy interface public interface PaymentStrategy { void pay(BigDecimal amount); } // Concrete strategies public class CreditCardPayment implements PaymentStrategy { private final String cardNumber; public void pay(BigDecimal amount) { // Charge credit card } } public class PayPalPayment implements PaymentStrategy { private final String email; public void pay(BigDecimal amount) { // PayPal payment } } public class CryptoPayment implements PaymentStrategy { public void pay(BigDecimal amount) { // Crypto payment } } // Context uses the strategy public class ShoppingCart { private PaymentStrategy paymentStrategy; public void setPaymentStrategy(PaymentStrategy strategy) { this.paymentStrategy = strategy; } public void checkout() { BigDecimal total = calculateTotal(); paymentStrategy.pay(total); // Uses selected strategy } } // Usage ShoppingCart cart = new ShoppingCart(); cart.setPaymentStrategy(new CreditCardPayment("4111...")); cart.checkout(); cart.setPaymentStrategy(new PayPalPayment("user@email.com")); cart.checkout(); // Modern Java: lambdas work if strategy has single method cart.setPaymentStrategy(amount -> System.out.println("Paid: " + amount));
Problem: Object needs to notify other objects of state changes without tight coupling.
// Observer interface public interface OrderEventListener { void onOrderPlaced(Order order); void onOrderShipped(Order order); void onOrderCancelled(Order order); } // Subject (Observable) public class OrderService { private final List<OrderEventListener> listeners = new CopyOnWriteArrayList<>(); public void addListener(OrderEventListener listener) { listeners.add(listener); } public void removeListener(OrderEventListener listener) { listeners.remove(listener); } public void placeOrder(Order order) { // Business logic saveOrder(order); // Notify all observers listeners.forEach(l -> l.onOrderPlaced(order)); } } // Concrete observers public class EmailNotifier implements OrderEventListener { public void onOrderPlaced(Order order) { sendEmail(order.getCustomer(), "Order confirmed!"); } // ... other methods } public class InventoryUpdater implements OrderEventListener { public void onOrderPlaced(Order order) { decrementStock(order.getItems()); } // ... other methods } // Usage OrderService orderService = new OrderService(); orderService.addListener(new EmailNotifier()); orderService.addListener(new InventoryUpdater()); orderService.addListener(new AnalyticsTracker()); orderService.placeOrder(order); // All listeners notified
// Modern approach: Spring Events (decoupled, async-capable) // Event class public record OrderPlacedEvent(Order order, Instant timestamp) {} // Publisher @Service public class OrderService { private final ApplicationEventPublisher eventPublisher; public void placeOrder(Order order) { saveOrder(order); eventPublisher.publishEvent(new OrderPlacedEvent(order, Instant.now())); } } // Listeners (completely decoupled from publisher) @Component public class EmailNotifier { @EventListener public void handleOrderPlaced(OrderPlacedEvent event) { sendEmail(event.order().getCustomer(), "Order confirmed!"); } } @Component public class InventoryUpdater { @EventListener @Async // Run in separate thread! public void handleOrderPlaced(OrderPlacedEvent event) { decrementStock(event.order().getItems()); } } // Benefits over classic Observer: // - No need to manually register listeners // - Can be async // - Transactional events possible // - Listeners can be conditional
Problem: Define algorithm structure, but let subclasses customize specific steps.
// Abstract class defines the algorithm skeleton public abstract class DataProcessor { // Template method - defines algorithm structure // Marked FINAL so subclasses can't change the flow public final void process() { Data raw = readData(); // Step 1 Data validated = validate(raw); // Step 2 Data processed = transform(validated); // Step 3 save(processed); // Step 4 notifyComplete(); // Step 5 (hook) } // Abstract methods - subclasses MUST implement protected abstract Data readData(); protected abstract Data transform(Data data); // Concrete methods - shared implementation protected Data validate(Data data) { if (data == null) { throw new IllegalArgumentException("Data cannot be null"); } return data; } protected abstract void save(Data data); // Hook - optional override (empty default) protected void notifyComplete() { // Default: do nothing } } // Concrete implementation public class CsvDataProcessor extends DataProcessor { @Override protected Data readData() { return parseCsvFile(); // CSV-specific } @Override protected Data transform(Data data) { return cleanCsvData(data); // CSV-specific } @Override protected void save(Data data) { saveToDatabase(data); } } public class JsonDataProcessor extends DataProcessor { @Override protected Data readData() { return parseJsonFile(); // JSON-specific } @Override protected Data transform(Data data) { return normalizeJson(data); // JSON-specific } @Override protected void save(Data data) { saveToDatabase(data); } @Override protected void notifyComplete() { sendSlackNotification(); // Custom hook implementation } } // Usage - algorithm flow is guaranteed DataProcessor processor = new CsvDataProcessor(); processor.process(); // Always: read → validate → transform → save → notify
Problem: Classes shouldn't create their own dependencies (hard to test, tightly coupled).
// BAD: Class creates its own dependencies public class OrderService { private final OrderRepository repository = new PostgresOrderRepository(); private final EmailService emailService = new SmtpEmailService(); private final PaymentGateway payment = new StripePaymentGateway(); public void placeOrder(Order order) { repository.save(order); payment.charge(order.getTotal()); emailService.send(order.getCustomer(), "Order confirmed"); } } // Problems: // - Can't test without real database, payment, email // - Can't swap implementations (e.g., MockPaymentGateway) // - Hard-coded dependencies = tight coupling
// GOOD: Dependencies are injected (passed in) public class OrderService { private final OrderRepository repository; private final EmailService emailService; private final PaymentGateway payment; // Constructor injection - dependencies are explicit public OrderService( OrderRepository repository, EmailService emailService, PaymentGateway payment) { this.repository = repository; this.emailService = emailService; this.payment = payment; } public void placeOrder(Order order) { repository.save(order); payment.charge(order.getTotal()); emailService.send(order.getCustomer(), "Order confirmed"); } } // Production OrderService service = new OrderService( new PostgresOrderRepository(), new SmtpEmailService(), new StripePaymentGateway() ); // Testing - inject mocks! OrderService testService = new OrderService( new InMemoryOrderRepository(), mock(EmailService.class), new MockPaymentGateway() );
// Spring handles wiring automatically @Service public class OrderService { private final OrderRepository repository; private final EmailService emailService; private final PaymentGateway payment; // Spring auto-injects matching beans public OrderService( OrderRepository repository, EmailService emailService, PaymentGateway payment) { this.repository = repository; this.emailService = emailService; this.payment = payment; } } // Dependencies are beans too @Repository public class PostgresOrderRepository implements OrderRepository { } @Service public class SmtpEmailService implements EmailService { } @Component public class StripePaymentGateway implements PaymentGateway { } // In tests, override with test beans @TestConfiguration public class TestConfig { @Bean public PaymentGateway paymentGateway() { return new MockPaymentGateway(); } }
Dependency Injection is the foundation of testable, maintainable enterprise code. It's not about frameworks — it's about designing classes that receive their dependencies rather than creating them. Spring/Guice just automate the wiring.
| Pattern | When to Use | Java Example |
|---|---|---|
| Singleton | Single instance needed (config, pools) | Enum singleton, Spring @Component |
| Factory | Decouple object creation from usage | List.of(), Optional.of() |
| Builder | Complex object with many optional params | StringBuilder, HttpRequest.Builder |
| Adapter | Make incompatible interfaces work together | Arrays.asList(), InputStreamReader |
| Decorator | Add behavior without changing class | BufferedInputStream, Collections.unmodifiableList() |
| Proxy | Control access, lazy loading, caching | Spring @Transactional, @Cacheable |
| Strategy | Swap algorithms at runtime | Comparator, Collector |
| Observer | Event-driven communication | PropertyChangeListener, Spring Events |
| Template Method | Define algorithm skeleton, customize steps | AbstractList, HttpServlet |