Design Patterns
Design patterns are named solutions to recurring design problems — not copy-paste code, but shared vocabulary that lets engineers communicate architecture in one word instead of…
Introduction
Design patterns are named solutions to recurring design problems — not copy-paste code, but shared vocabulary that lets engineers communicate architecture in one word instead of twenty. When a staff engineer says "use Strategy for payment rails" or "Observer for side effects," the team immediately understands structure, trade-offs, and Spring wiring.
Enterprise Java — especially Spring Boot — is pattern-heavy by design. The Spring container itself is a Singleton registry and Factory. @Autowired injection implements Strategy. @EventListener is Observer. Legacy gateway adapters wrap third-party APIs with the Adapter pattern. Lombok-free domain objects use Builder for complex construction.
This lesson covers six patterns every Java engineer must recognize in production codebases: Singleton, Factory, Builder, Strategy, Observer, and Adapter — with banking examples and Spring Framework mappings you will encounter from day one on a fintech team.
Business problem
Teams without pattern vocabulary rebuild the same broken designs:
- Switch statements everywhere: Payment type routing via if/else — adding BNPL means editing 12 files instead of one new Strategy bean.
- Tight coupling: TransferService directly instantiates legacy SOAP client — vendor migration requires rewriting business logic.
- Constructor hell:
new Payment(id, amount, currency, fee, tax, metadata, ...)with 15 parameters — wrong argument order causes silent production bugs. - Side-effect spaghetti: Transfer method calls audit, email, fraud, analytics inline — one failure rolls back entire transaction.
- Multiple singletons: Hand-rolled
DatabaseConnection.getInstance()fights Spring's singleton scope — double instances, connection leaks.
Why this topic exists
Patterns exist to capture proven solutions and prevent repeated mistakes:
- Shared vocabulary: "Strategy" in a design doc beats three paragraphs of interface + impl explanation.
- Change isolation: Patterns encode SOLID — Strategy enables OCP, Observer enables SRP for side effects.
- Framework alignment: Spring Boot idioms map directly to Gang of Four patterns — learn both together.
- Interview and review standard: Staff loops expect pattern recognition — refactor suggestions reference patterns by name.
- Legacy integration: Adapter pattern is how enterprises wrap 20-year-old mainframe APIs without contaminating modern services.
Core concepts
Six essential patterns — problem, solution, Spring mapping:
- Singleton: Exactly one instance. Problem: shared config/connection pool. Solution: private constructor + static accessor. Spring: default bean scope is singleton — prefer container over hand-rolled.
- Factory: Create objects without exposing construction logic. Problem: complex instantiation rules. Solution:
PaymentProcessorFactory.create(type). Spring:@Beanmethods in @Configuration are factories. - Builder: Construct complex objects step-by-step. Problem: telescoping constructors. Solution: fluent API
Payment.builder().amount().currency().build(). Spring: test data builders; domain object construction. - Strategy: Encapsulate interchangeable algorithms. Problem: payment rail varies by type. Solution:
PaymentProcessorinterface + ACH/Wire impls. Spring: inject interface; @Profile selects strategy. - Observer: Notify dependents on state change. Problem: transfer triggers audit, email, fraud. Solution: publish event, listeners react. Spring:
ApplicationEventPublisher+@EventListener. - Adapter: Convert incompatible interface to expected one. Problem: legacy SOAP API vs modern REST service. Solution:
LegacyPaymentAdapter implements PaymentProcessor. Spring: @Service adapter wrapping external client.
Internal architecture
Pattern composition in a payment microservice:
┌──────────────── PaymentController ──────────────────────────────────────────┐│ POST /transfer → TransferService.transfer(PaymentRequest) │└───────────────────────────────┬───────────────────────────────────────────────┘│┌───────────▼───────────┐│ TransferService │ ← depends on abstractions└───────────┬───────────┘│┌──────────────────────┼──────────────────────┐│ Strategy │ Factory │ Observer▼ ▼ ▼PaymentProcessor ProcessorFactory ApplicationEventPublisher(interface) .create(type) ││ │ ├── @EventListener AuditListener├── ACHProcessor │ ├── @EventListener EmailListener├── WireProcessor │ └── @EventListener FraudListener└── LegacyAdapter ─────┘ (side effects decoupled)(Adapter wraps SOAP client)Payment built via Builder:Payment.builder().from().to().amount().currency().build()Spring Singleton scope:@Service beans = one instance per container (DatabaseConfig, TransferService)
Six pattern diagrams — structure at a glance:
Code walkthrough
All six patterns in one banking demo — with expected output:
- Singleton (line 6): Double-checked locking shown for interviews — in Spring Boot use @Service singleton scope instead.
- Builder (line 22): Fluent API eliminates 15-parameter constructor; immutable Payment after build.
- Strategy (line 48): TransferService depends on PaymentProcessor — ACH vs Wire swapped without code change.
- Adapter (line 68): LegacySoapClient unchanged; LegacyPaymentAdapter translates to PaymentProcessor contract.
- Factory (line 78): Client calls create("ACH") — construction logic centralized; maps to @Bean factory methods.
- Observer (line 91): TransferService publishes event; Audit and Email react independently — maps to @EventListener.
import java.math.BigDecimal;import java.util.*;// ═══ 1. SINGLETON — Spring manages this; hand-rolled shown for recognition ═══class AuditLogConfig {private static volatile AuditLogConfig instance;private AuditLogConfig() {}static AuditLogConfig getInstance() {if (instance == null) synchronized (AuditLogConfig.class) {if (instance == null) instance = new AuditLogConfig();}return instance;}String getFormat() { return "ISO-8601"; }}// Spring equivalent: @Service @Scope("singleton") — never roll your own in Boot// ═══ 2. BUILDER — complex Payment construction ═══class Payment {private final String from, to, currency;private final BigDecimal amount;private Payment(Builder b) {this.from = b.from; this.to = b.to;this.amount = b.amount; this.currency = b.currency;}static Builder builder() { return new Builder(); }static class Builder {private String from, to, currency = "USD";private BigDecimal amount;Builder from(String f) { from = f; return this; }Builder to(String t) { to = t; return this; }Builder amount(BigDecimal a) { amount = a; return this; }Builder currency(String c) { currency = c; return this; }Payment build() { return new Payment(this); }}@Override public String toString() {return from + " → " + to + " " + currency + " " + amount;}String fromAccount() { return from; }String toAccount() { return to; }BigDecimal amountValue() { return amount; }}// ═══ 3. STRATEGY — interchangeable payment processors ═══interface PaymentProcessor {String getName();boolean process(Payment payment);}class ACHProcessor implements PaymentProcessor {public String getName() { return "ACH"; }public boolean process(Payment p) {System.out.println(" [Strategy-ACH] Processing " + p);return true;}}class WireProcessor implements PaymentProcessor {public String getName() { return "WIRE"; }public boolean process(Payment p) {System.out.println(" [Strategy-WIRE] SWIFT " + p);return true;}}// ═══ 4. ADAPTER — wrap legacy SOAP client behind modern interface ═══class LegacySoapClient {boolean sendWire(String from, String to, String amount) {System.out.println(" [Legacy-SOAP] wire(" + from + "," + to + "," + amount + ")");return true;}}class LegacyPaymentAdapter implements PaymentProcessor {private final LegacySoapClient soap = new LegacySoapClient();public String getName() { return "LEGACY-ADAPTER"; }public boolean process(Payment p) {return soap.sendWire(p.fromAccount(), p.toAccount(), p.amountValue().toPlainString());}}// ═══ 5. FACTORY — create processor without caller knowing impl ═══class PaymentProcessorFactory {static PaymentProcessor create(String type) {return switch (type.toUpperCase()) {case "ACH" -> new ACHProcessor();case "WIRE" -> new WireProcessor();case "LEGACY" -> new LegacyPaymentAdapter();default -> throw new IllegalArgumentException("Unknown: " + type);};}}// Spring equivalent: @Bean PaymentProcessor processor(@Value("${rail}") String type)// ═══ 6. OBSERVER — publish/subscribe for side effects ═══class PaymentEvent { final Payment payment; PaymentEvent(Payment p) { payment = p; } }interface PaymentListener { void onPayment(PaymentEvent event); }class AuditListener implements PaymentListener {public void onPayment(PaymentEvent e) {System.out.println(" [Observer-Audit] Logged: " + e.payment);}}class EmailListener implements PaymentListener {public void onPayment(PaymentEvent e) {System.out.println(" [Observer-Email] Receipt sent to " + e.payment.fromAccount());}}class TransferService {private final PaymentProcessor processor;private final List<PaymentListener> listeners = new ArrayList<>();TransferService(PaymentProcessor processor) { this.processor = processor; }void addListener(PaymentListener l) { listeners.add(l); }void transfer(Payment payment) {processor.process(payment); // StrategyPaymentEvent event = new PaymentEvent(payment);listeners.forEach(l -> l.onPayment(event)); // Observer}}class PatternsDemo {public static void main(String[] args) {System.out.println("Singleton format: " + AuditLogConfig.getInstance().getFormat());Payment payment = Payment.builder().from("ACC-001").to("ACC-002").amount(new BigDecimal("500.00")).build(); // Builder// Factory + StrategyPaymentProcessor processor = PaymentProcessorFactory.create("ACH");TransferService service = new TransferService(processor);service.addListener(new AuditListener());service.addListener(new EmailListener());System.out.println("Transfer via " + processor.getName() + ":");service.transfer(payment);// Adapter demoSystem.out.println("Transfer via Legacy Adapter:");TransferService legacy = new TransferService(PaymentProcessorFactory.create("LEGACY"));legacy.addListener(new AuditListener());legacy.transfer(payment);}}/** Expected output:* Singleton format: ISO-8601* Transfer via ACH:* [Strategy-ACH] Processing ACC-001 → ACC-002 USD 500.00* [Observer-Audit] Logged: ACC-001 → ACC-002 USD 500.00* [Observer-Email] Receipt sent to ACC-001* Transfer via Legacy Adapter:* [Legacy-SOAP] wire(ACC-001,ACC-002,500.00)* [Observer-Audit] Logged: ACC-001 → ACC-002 USD 500.00*/
Production example
Spring Boot — all six patterns in production wiring:
- @Service singleton: Spring container guarantees one TransferService instance — no getInstance() needed.
- PaymentProcessorFactory @Bean: Centralizes rail→processor lookup; inject List<PaymentProcessor> for auto-discovery.
- @EventListener @Async: Observer pattern — email/audit/fraud run async without blocking transfer response.
- LegacySoapPaymentAdapter: Adapter isolates SOAP quirks — swap legacy system by changing adapter only.
// BUILDER — domain object (or record + factory for simpler cases)Payment payment = Payment.builder().from(cmd.fromAccount()).to(cmd.toAccount()).amount(cmd.amount()).currency("USD").build();// SINGLETON — @Service is singleton by default (one instance per container)@Servicepublic class TransferService {private final PaymentProcessorFactory factory;private final ApplicationEventPublisher events; // Observer publisherpublic TransferService(PaymentProcessorFactory factory,ApplicationEventPublisher events) {this.factory = factory;this.events = events;}@Transactionalpublic PaymentResult transfer(PaymentCommand cmd) {Payment payment = cmd.toPayment();PaymentProcessor processor = factory.getProcessor(cmd.rail()); // Factory + Strategyprocessor.process(payment);events.publishEvent(new PaymentCompletedEvent(payment)); // Observerreturn PaymentResult.success();}}// FACTORY — @Configuration creates Strategy beans@Configurationpublic class PaymentConfig {@Beanpublic PaymentProcessorFactory processorFactory(List<PaymentProcessor> processors) {return new PaymentProcessorFactory(processors); // map rail → processor}}// STRATEGY — one @Service per rail@Service("ach")public class ACHPaymentProcessor implements PaymentProcessor { ... }// ADAPTER — legacy integration@Servicepublic class LegacySoapPaymentAdapter implements PaymentProcessor {private final LegacySoapClient soapClient; // injected legacy beanpublic PaymentResult process(Payment p) {soapClient.sendWire(p.toSoapRequest()); // adapt interface}}// OBSERVER — decoupled side effects@Componentpublic class PaymentAuditListener {@EventListener@Asyncpublic void onPaymentCompleted(PaymentCompletedEvent event) {auditRepository.save(event.toAuditEntry());}}
Enterprise case study
Goldman Sachs payment modernization — Adapter + Strategy: A tier-1 bank ran core wire transfers through a 1990s SOAP mainframe interface while new microservices spoke REST. Teams used Adapter to wrap SOAP clients behind PaymentProcessor (Strategy). When the bank migrated to a modern API gateway, they replaced only the Adapter implementation — 40 downstream services unchanged. Side effects (SWIFT confirmation emails, regulatory filings, fraud scoring) migrated from inline calls to Observer via Kafka events (Spring Cloud Stream), reducing transfer latency from 800ms to 120ms.
- Before: Monolithic transfer method — SOAP call + 6 inline side effects + switch on payment type.
- Patterns applied: Strategy (rails), Adapter (SOAP), Observer (Kafka events), Factory (rail routing), Builder (Payment DTO).
- After: New rail = new Strategy bean; legacy swap = new Adapter; new side effect = new @EventListener.
- Result: 12 payment rails supported; zero modifications to TransferService in 18 months.
Performance considerations
Pattern performance in production:
- Singleton: One instance reduces memory — but stateful singletons are thread-safety traps; prefer stateless @Service beans.
- Observer async: @Async @EventListener decouples latency — transfer returns before email sends; watch thread pool sizing.
- Factory lookup: Map-based factory O(1) — negligible vs payment gateway API call (50-200ms).
- Builder allocation: One Builder object per request — trivial vs DB round-trip; reuse builder only in benchmarks, not production.
- Adapter overhead: Thin adapter = one extra method call (~ns); heavy adapter doing transformation = measure and cache.
Security considerations
Pattern misuse creates security gaps:
- Singleton with mutable state: Hand-rolled singleton storing user session — cross-request data leak in servlet container.
- Observer ordering: Fraud check must run before settlement — use @Order on @EventListener or synchronous listener for security-critical path.
- Factory injection: Never let user input directly select factory type without allowlist — injection attack vector.
- Adapter trust boundary: Legacy adapter parsing SOAP responses — validate/sanitize before passing to domain layer.
Scalability considerations
Patterns enable horizontal scale:
- Stateless Strategy beans: TransferService + PaymentProcessor scale horizontally behind load balancer — no singleton state.
- Observer → message broker: @EventListener fine for monolith; at scale replace with Kafka/RabbitMQ for cross-service Observer.
- Factory + @ConditionalOnProperty: Enable payment rails per region via config — US gets ACH, EU gets SEPA, no code deploy.
- Builder for immutable events: PaymentCompletedEvent immutable — safe to publish across threads and services.
Production challenges
Common pattern violations and failures:
- Singleton abuse: DatabaseConnection.getInstance() + Spring DataSource — two connection pools, exhaustion under load.
- Strategy via switch: switch(rail) in TransferService instead of injected Strategy — OCP violation masquerading as "simple code."
- Observer hell: 30 @EventListener on same event — ordering bugs, circular events, debugging nightmare. Consolidate or use saga.
- Leaky Adapter: Adapter exposes SOAP types to domain layer — contamination spreads; adapter must translate to domain types.
- Builder without validation: build() creates invalid Payment — validate in build() or compact constructor.
Common mistakes
- Hand-rolling Singleton in Spring Boot — container already manages singleton scope; double lifecycle.
- Using Builder for 2-field objects — overkill; use record or constructor.
- Strategy interface with 20 methods — fat strategy; split interfaces (ISP).
- Synchronous Observer for slow side effects — blocks main flow; use @Async or message queue.
- Adapter that passthroughs legacy exceptions — wrap in domain exceptions at adapter boundary.
Debugging guide
Debug pattern-related issues:
- Wrong processor invoked: Log factory selection — which rail mapped to which Strategy bean? Check @Primary and @Qualifier conflicts.
- Listener not firing: @EventListener requires same application context; @TransactionalEventListener fires after commit — check phase.
- Double singleton: grep getInstance() — should not coexist with @Service for same resource.
- Adapter timeout: Legacy SOAP slow — add circuit breaker (Resilience4j) at adapter, not in TransferService.
# Find hand-rolled singletons competing with Springgrep -rn "getInstance\|private static.*instance" src/ --include="*.java"# Find switch-based strategy anti-patterngrep -rn "switch.*[Rr]ail\|switch.*[Pp]ayment[Tt]ype" src/ --include="*.java"# Spring: log bean creation# logging.level.org.springframework.beans.factory=DEBUG
Best practices
- Use Spring @Service singleton scope — never hand-roll Singleton in Boot apps.
- Strategy via injected interface + @Profile/@Qualifier — not switch statements.
- Observer via ApplicationEventPublisher + @EventListener — async for non-critical side effects.
- Adapter at infrastructure boundary — translate legacy types to domain types immediately.
- Factory as @Configuration @Bean or dedicated factory with allowlisted types.
- Builder for objects with 4+ optional parameters — validate in build().
- Prefer composition of patterns — Strategy + Observer + Adapter compose cleanly in Spring.
Anti-patterns
- God Factory: One factory creates every object in the system — split by domain.
- Singleton with mutable fields: Shared counters, caches without sync — use Redis or scoped beans.
- Anemic Observer: Event carries no data — listeners re-query DB; events should carry sufficient payload.
- Strategy explosion: 50 strategy classes with one line difference — consider policy objects or configuration-driven behavior.
- Adapter returning null: Silent failure from legacy system — wrap in Optional or throw domain exception.
Staff engineer notes
- Spring Boot is a pattern implementation toolkit — learn patterns to understand Spring, not vice versa.
- In architecture review, name the pattern: "This is Strategy" or "This needs an Adapter" — speeds decisions.
- Observer at scale becomes event-driven architecture — @EventListener is training wheels for Kafka.
- Hand-rolled Singleton in a Spring app is a code smell — almost always wrong unless truly JVM-global resource.
- Builder pattern + immutable objects = thread-safe DTOs without synchronized blocks — default for domain events.
Interview questions
Interview preparation
15 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What is the Singleton pattern and how does Spring implement it?
BeginnerModel answer
- Ensures one instance exists. Hand-rolled: private constructor + static getInstance(). Spring: default @Service/@Component scope is singleton
- container creates one bean per context. Prefer Spring over hand-rolled in Boot apps.
Follow-up probe
Thread-safe singleton?
2Explain the Factory pattern with a Java example.
BeginnerModel answer
- Encapsulates object creation. PaymentProcessorFactory.create('ACH') returns appropriate processor without caller knowing concrete class. Spring @Bean methods in @Configuration are factory methods
- container calls them to create beans.
Follow-up probe
Factory vs Abstract Factory?
3When should you use the Builder pattern?
BeginnerModel answer
- Constructing objects with many optional parameters
- avoids telescoping constructors and unclear argument order. Payment.builder().from().to().amount().currency().build(). Validate in build(). Use for 4+ params; records suffice for simple immutable data.
Follow-up probe
Builder vs constructor?
4Explain Strategy pattern and its Spring equivalent.
BeginnerModel answer
Define family of algorithms (payment processors), encapsulate each, make interchangeable.
TransferService depends on PaymentProcessor interface.
Spring injects concrete @Service impl via constructor.
@Profile or @Qualifier selects strategy.
Follow-up probe
Strategy vs State pattern?
5Explain Observer pattern in Spring Boot.
BeginnerModel answer
Subject (TransferService) publishes events without knowing listeners.
Observers (@EventListener methods) react independently.
publishEvent() + @EventListener.
@Async for non-blocking.
Decouples audit, email, fraud from core transfer.
Follow-up probe
Event ordering?
Intermediate
6Explain Adapter pattern with enterprise example.
IntermediateModel answer
Convert incompatible interface to one client expects.
LegacySoapClient has sendWire(from,to,amount).
LegacyPaymentAdapter implements PaymentProcessor, translates process(Payment) to SOAP call.
Domain layer never imports SOAP types.
Follow-up probe
Adapter vs Facade?
7Why avoid hand-rolled Singleton in Spring Boot?
IntermediateModel answer
- Spring container already manages singleton lifecycle. Hand-rolled getInstance() creates second instance outside container
- breaks injection, testing, and lifecycle. Two DataSources, two connection pools. Use @Service scope singleton instead.
Follow-up probe
When is hand-rolled OK?
8How do Strategy and Factory work together?
IntermediateModel answer
Factory creates/selects appropriate Strategy based on input. PaymentProcessorFactory.create(rail) returns ACH or Wire processor. TransferService calls factory.getProcessor(cmd.rail()) then processor.process(). Spring can inject Mapkeyed by bean name. Follow-up probe
Inject Map of strategies?
9Compare @EventListener vs direct method calls.
IntermediateModel answer
Direct calls: tight coupling, TransferService knows all listeners, synchronous blocking.
@EventListener: loose coupling, add listener without editing publisher, @Async non-blocking.
Trade-off: eventual consistency, harder debugging, ordering complexity.
Follow-up probe
When keep direct calls?
10How would you test Strategy pattern services?
IntermediateModel answer
- Unit test: inject mock PaymentProcessor into TransferService
- verify process() called with correct Payment. Integration test: @MockBean PaymentProcessor in @WebMvcTest. No need for real ACH gateway
- Strategy interface enables mock substitution (DIP).
Follow-up probe
Test @EventListener?
Advanced
11Design payment system using all six patterns.
AdvancedModel answer
Builder: Payment construction.
Singleton: @Service beans.
Factory: PaymentProcessorFactory routes rail.
Strategy: ACH/Wire/Card processors.
Adapter: LegacySoapAdapter.
Observer: @EventListener for audit/email/fraud.
TransferService orchestrates without knowing concrete types.
Follow-up probe
Kafka replaces Observer?
12Refactor switch(paymentType) to Strategy + Factory.
AdvancedModel answer
1) Extract PaymentProcessor interface. 2) One class per case (ACHProcessor, etc.). 3) Factory maps type→processor (Map or switch inside factory only). 4) Inject factory into service. 5) Register as @Service beans. 6) Spring auto-wires List. New type = new class + factory registration. Follow-up probe
Remove factory switch entirely?
13Observer pattern at scale — when to move from @EventListener to Kafka?
AdvancedModel answer
- Stay @EventListener: monolith, same JVM, <10 listeners, strong consistency OK. Move to Kafka: microservices, cross-team events, replay needed, high volume (>1k events/sec), audit trail of events. Spring Cloud Stream bridges
- same Observer mental model, distributed transport.
Follow-up probe
Transactional outbox?
14Adapter wrapping legacy system — how handle failures?
AdvancedModel answer
Circuit breaker (Resilience4j) at adapter boundary.
Translate legacy error codes to domain exceptions (PaymentFailedException).
Never leak SOAPFault to domain.
Retry idempotent operations with @Retryable.
Timeout config on legacy client.
Fallback adapter for degraded mode.
Follow-up probe
Anti-corruption layer?
15Which patterns implement SOLID principles?
AdvancedModel answer
Strategy → OCP + DIP.
Observer → SRP (separate side effects).
Adapter → DIP (depend on PaymentProcessor not SOAP).
Factory → encapsulates creation (SRP).
Singleton → controlled single instance.
Builder → separates construction from representation (SRP).
Patterns are SOLID in practice.
Follow-up probe
Patterns that violate SOLID?
Hands-on exercise
Lab: Wire patterns in the playground — run demo, then extend:
- Run PatternsDemo — trace Singleton, Builder, Factory, Strategy, Observer, Adapter in output.
- Add CardProcessor implements PaymentProcessor — register in factory, run transfer without editing TransferService.
- Add FraudListener implements PaymentListener — observe Observer extension without publisher change.
- Write a comment mapping each class to its Spring Boot equivalent (@Service, @Bean, @EventListener).
- Bonus: replace hand-rolled Observer with a comment showing ApplicationEventPublisher version.
JavaDesign Patterns
Press Run to execute. Real javac + JVM via remote API. Falls back to simulator on network failure.Architecture trade-offs
- Hand-rolled Singleton vs Spring scope: Spring wins in Boot apps; hand-rolled only for non-Spring libraries.
- @EventListener vs Kafka: EventListener wins for monolith simplicity; Kafka wins for distributed scale and replay.
- Builder vs record: Builder wins for many optional fields; record wins for simple immutable carriers.
- Factory switch vs Map injection: Switch in factory OK at boundary; Map<String, Processor> from Spring auto-wiring scales better.
Summary
Design patterns are the shared vocabulary of enterprise Java — and Spring Boot implements them natively. You can now recognize Singleton, Factory, Builder, Strategy, Observer, and Adapter in production code, wire them with Spring annotations, and refactor switch statements and legacy integrations using the right pattern. Next: exception handling and clean error design.
Key takeaways
- Singleton: one instance — Spring @Service default scope; avoid hand-rolled getInstance() in Boot.
- Factory: centralize creation — @Bean methods or PaymentProcessorFactory.create(type).
- Builder: fluent construction for complex Payment objects — validate in build().
- Strategy: PaymentProcessor interface + ACH/Wire impls — Spring injects via constructor.
- Observer: publishEvent + @EventListener — decouple audit, email, fraud from transfer.
- Adapter: LegacySoapAdapter implements PaymentProcessor — wrap legacy without contaminating domain.