Java Fundamentals Tutorial 0/33 lessons ~6 min read Lesson 9

    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…

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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: @Bean methods 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: PaymentProcessor interface + 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:

    text
    ┌──────────────── 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:

    Singleton — one instance
    getInstance()
    Static accessor
    Single object
    Shared state
    Spring @Service
    Container singleton
    Prefer Spring singleton scope over hand-rolled getInstance().
    Factory — object creation
    Client
    Requests object
    Factory
    create(type)
    Product
    ACH or Wire impl
    @Bean @Configuration methods are Spring factories.
    Strategy — interchangeable algorithm
    Context
    TransferService
    Strategy
    PaymentProcessor
    ACH impl
    Concrete A
    Wire impl
    Concrete B
    Spring injects strategy via constructor — @Profile selects impl.
    Observer — event notification
    Publisher
    TransferService
    Event
    PaymentCompleted
    AuditListener
    Observer 1
    EmailListener
    Observer 2
    @EventListener methods react without modifying publisher.

    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.
    java
    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); // Strategy
    PaymentEvent 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 + Strategy
    PaymentProcessor 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 demo
    System.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.
    java
    // 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)
    @Service
    public class TransferService {
    private final PaymentProcessorFactory factory;
    private final ApplicationEventPublisher events; // Observer publisher
    public TransferService(PaymentProcessorFactory factory,
    ApplicationEventPublisher events) {
    this.factory = factory;
    this.events = events;
    }
    @Transactional
    public PaymentResult transfer(PaymentCommand cmd) {
    Payment payment = cmd.toPayment();
    PaymentProcessor processor = factory.getProcessor(cmd.rail()); // Factory + Strategy
    processor.process(payment);
    events.publishEvent(new PaymentCompletedEvent(payment)); // Observer
    return PaymentResult.success();
    }
    }
    // FACTORY — @Configuration creates Strategy beans
    @Configuration
    public class PaymentConfig {
    @Bean
    public 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
    @Service
    public class LegacySoapPaymentAdapter implements PaymentProcessor {
    private final LegacySoapClient soapClient; // injected legacy bean
    public PaymentResult process(Payment p) {
    soapClient.sendWire(p.toSoapRequest()); // adapt interface
    }
    }
    // OBSERVER — decoupled side effects
    @Component
    public class PaymentAuditListener {
    @EventListener
    @Async
    public 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.
    bash
    # Find hand-rolled singletons competing with Spring
    grep -rn "getInstance\|private static.*instance" src/ --include="*.java"
    # Find switch-based strategy anti-pattern
    grep -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

    5
    1. 1What is the Singleton pattern and how does Spring implement it?
      Beginner

      Model 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?

    2. 2Explain the Factory pattern with a Java example.
      Beginner

      Model 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?

    3. 3When should you use the Builder pattern?
      Beginner

      Model 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?

    4. 4Explain Strategy pattern and its Spring equivalent.
      Beginner

      Model 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?

    5. 5Explain Observer pattern in Spring Boot.
      Beginner

      Model 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

    5
    1. 6Explain Adapter pattern with enterprise example.
      Intermediate

      Model 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?

    2. 7Why avoid hand-rolled Singleton in Spring Boot?
      Intermediate

      Model 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?

    3. 8How do Strategy and Factory work together?
      Intermediate

      Model 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 Map keyed by bean name.

      Follow-up probe

      Inject Map of strategies?

    4. 9Compare @EventListener vs direct method calls.
      Intermediate

      Model 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?

    5. 10How would you test Strategy pattern services?
      Intermediate

      Model 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

    5
    1. 11Design payment system using all six patterns.
      Advanced

      Model 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?

    2. 12Refactor switch(paymentType) to Strategy + Factory.
      Advanced

      Model 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?

    3. 13Observer pattern at scale — when to move from @EventListener to Kafka?
      Advanced

      Model 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?

    4. 14Adapter wrapping legacy system — how handle failures?
      Advanced

      Model 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?

    5. 15Which patterns implement SOLID principles?
      Advanced

      Model 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

    Starter Templates
    OutputRemote JVM (Piston · Java 15)
    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.
    Ready to mark this lesson complete?Track your journey across the entire course.