Introduction: Why Design Patterns Matter in 2025
Design patterns remain the foundation of maintainable, scalable Java applications — providing battle-tested solutions to recurring software design problems. With Java 21+ introducing sealed classes, records, pattern matching, and virtual threads, classic patterns have evolved while their core principles endure.
This guide covers the essential creational, structural, and behavioural patterns, their modern Java implementations, common anti-patterns to avoid, testing strategies for pattern-heavy code, and enterprise application patterns that power production systems at scale.
Creational Patterns: Object Construction
Master object creation strategies:
- Singleton (Modern): Use
enumsingletons for thread safety and serialisation guarantees —public enum DatabaseConnection { INSTANCE; }. Avoid double-checked locking and static holder patterns. For Spring applications,@Componentbeans are singletons by default, eliminating manual implementation. - Builder: Essential for objects with many optional parameters. Java records + builder pattern: define a record for immutability and a nested Builder class for flexible construction. Lombok's
@Buildergenerates builders automatically. Use for configuration objects, DTOs, and API request/response models. - Factory Method: Define an interface for object creation, letting subclasses decide which class to instantiate. Modern implementation uses sealed interfaces with pattern matching:
sealed interface Shape permits Circle, Rectangle— the factory usesswitchpattern matching to create instances based on input type. - Abstract Factory: Create families of related objects — database access layer factories that produce MySQL, PostgreSQL, or MongoDB implementations. Spring's
@ConditionalOnPropertyacts as a configuration-driven abstract factory, selecting implementation families based on environment. - Prototype: Clone existing objects instead of creating new ones — useful for expensive-to-construct objects. Java's
Cloneableinterface is problematic; prefer copy constructors orrecordwith method — records are naturally immutable, making copying straightforward with modified fields.
Structural Patterns: Class Composition
Build flexible class hierarchies and compositions:
- Adapter: Bridge incompatible interfaces — wrap legacy SOAP services with REST-compatible adapters, convert third-party library data models to domain objects, or adapt different database driver APIs behind a unified repository interface. Use composition (wrapping) over inheritance for adapter flexibility.
- Decorator: Add responsibilities dynamically without subclassing — Java I/O streams exemplify this:
BufferedReader(InputStreamReader(FileInputStream(...))). Modern use: add caching, logging, retry, or circuit-breaking behaviour to service methods by wrapping them in decorator classes implementing the same interface. - Facade: Simplify complex subsystem interfaces — a
PaymentFacadehides the complexity of fraud detection, payment gateway selection, currency conversion, tax calculation, and receipt generation behind a singleprocessPayment()method. Spring's@Serviceclasses often serve as facades. - Proxy: Control access to objects — lazy loading (load expensive resources on first access), security (check permissions before method invocation), caching (return cached results for repeated calls). Spring AOP uses JDK dynamic proxies and CGLIB proxies for
@Transactional,@Cacheable, and@Securedannotations. - Composite: Treat individual objects and compositions uniformly — file system trees, UI component hierarchies, organisational structures. Use sealed interfaces with records for type-safe composite trees that the compiler can verify for exhaustiveness.
Behavioral Patterns: Object Communication
Manage algorithm and responsibility distribution:
- Observer: Decouple event producers from consumers — when a domain entity changes, notify all interested subscribers without the entity knowing who's listening. Modern implementation: Spring's
ApplicationEventPublisherwith@EventListenerannotations replace manual observer registration. - Strategy: Define interchangeable algorithms — payment processing (credit card, PayPal, crypto), sorting algorithms, compression methods. Java implementation: define a
@FunctionalInterfacefor the strategy, pass lambdas or method references instead of strategy classes, and useMapfor runtime selection. - Chain of Responsibility: Pass requests through a chain of handlers — validation pipelines (format check → business rules → authorisation), servlet filters, Spring Security filter chains. Each handler decides whether to process the request or pass it to the next handler in the chain.
- Template Method: Define algorithm skeleton in a base class, letting subclasses override specific steps. Modern alternative: use default methods in interfaces combined with strategy injection — more flexible than inheritance-based templates and easier to test.
- Command: Encapsulate requests as objects — enables undo/redo, command queuing, macro recording, and transactional operations. Combine with the Memento pattern for state snapshots that support full undo history.
Modern Java Patterns: Records, Sealed Classes, and Pattern Matching
Leverage Java 21+ features for elegant pattern implementations:
- Sealed Classes + Pattern Matching: Replace the Visitor pattern with exhaustive
switchexpressions —sealed interface Shape permits Circle, Rectangle, Triangleletsswitch (shape) { case Circle c -> ...; case Rectangle r -> ...; }handle all subtypes with compiler-verified exhaustiveness. No need for accept/visit boilerplate. - Records as Value Objects: Records enforce immutability and provide
equals(),hashCode(), andtoString()— perfect for Value Object, DTO, and Event patterns. Records with compact canonical constructors validate invariants at construction time without separate validation logic. - Functional Patterns: Java's functional interfaces enable patterns without class hierarchies —
Functionfor Strategy,Supplierfor Factory,UnaryOperatorfor Decorator chains. Compose functions withandThen()andcompose()for pipeline patterns. - Optional as Monad:
Optionalimplements the monad pattern —map(),flatMap(),filter(), andor()chain transformations safely without null checks. Use for return types, never for method parameters or fields. - Stream Pipelines: Streams implement the Pipeline/Chain pattern —
filter(),map(),reduce()compose data transformations declaratively.Collectorsimplement the Builder pattern for aggregation results. Parallel streams use the Fork/Join pattern internally.
Transform Your Publishing Workflow
Our experts can help you build scalable, API-driven publishing systems tailored to your business.
Anti-Patterns and Common Mistakes
Avoid pattern misuse that harms code quality:
- Pattern Overuse: Not every problem needs a pattern — applying Strategy for a single algorithm, Builder for objects with 2 fields, or Observer for single-subscriber scenarios adds complexity without benefit. Use patterns when they solve genuine design problems, not as resume-driven development.
- God Object: A Singleton that accumulates responsibilities becomes a God Object — violating Single Responsibility Principle. Split into focused services and use dependency injection instead of global state. Spring's IoC container eliminates most Singleton motivations.
- Inheritance Abuse: Deep class hierarchies using Template Method or inheritance-based patterns become rigid and hard to modify. Prefer composition over inheritance — Strategy with injection over Template Method with subclassing. Java's sealed interfaces enable safe, shallow hierarchies.
- Premature Abstraction: Creating abstract factories, decorators, and strategies before understanding actual requirements leads to over-engineered, hard-to-navigate code. Follow the Rule of Three — introduce a pattern when you see the same problem three times, not on the first occurrence.
- Pattern Naming Confusion: Naming classes
OrderFactory,PaymentStrategy, orUserObserverwhen they don't actually implement those patterns creates confusion. Use pattern names only when the implementation genuinely follows the pattern's structure and intent.
Testing Design Pattern Implementations
Ensure pattern-heavy code is thoroughly tested:
- Strategy Testing: Test each strategy implementation independently — verify algorithm correctness in isolation. Test strategy selection logic separately — ensure the correct strategy is chosen for given inputs. Mock strategies in consumer tests to verify delegation without testing strategy internals.
- Observer Testing: Verify observer registration/deregistration, notification delivery to all subscribers, ordering guarantees (if any), and error handling when observers throw exceptions. Use ArgumentCaptor in Mockito to verify event payloads delivered to observers.
- Builder Testing: Test required field validation (builder should reject incomplete objects), default values for optional fields, immutability of built objects, and edge cases (empty collections, null optional fields). Verify that the builder can construct objects with any combination of optional fields.
- Factory Testing: Test each factory variant produces the correct type, input validation rejects invalid creation parameters, factory caching (if implemented) returns the same instance, and error handling for unsupported types. Use parameterised tests for systematic factory variant coverage.
- Integration Testing: Test pattern combinations — Strategy selected by Factory, Decorator wrapping Singleton, Observer triggered by Command. Verify patterns compose correctly and don't interfere. Use test containers for patterns interacting with external systems.
Conclusion and MDS Java Architecture Services
Design patterns provide the architectural vocabulary for building enterprise Java applications. Key principles:
- Start simple — introduce patterns when complexity demands them, not preemptively.
- Embrace modern Java — sealed classes, records, and pattern matching simplify classic patterns while improving type safety.
- Favour composition — Strategy, Decorator, and Adapter with composition over Template Method and inheritance chains.
- Test systematically — each pattern component independently, then verify integration behaviour.
MetaDesign Solutions provides expert Java architecture and design services — from pattern-driven application design and codebase refactoring through microservice architecture implementation, legacy system modernisation with modern Java patterns, code review and architectural assessment, team training on design patterns and SOLID principles, and ongoing technical mentorship for Java engineering teams.




