B
🎭 Part 3 of 4 — Behavioral I

GoF Behavioral
Patterns I

6 behavioral patterns with production Java examples, Spring internals mapped to each pattern, deeper interview Q&As (3–5 each), and dedicated "Used in Frameworks" sections. Behavioral patterns define HOW objects collaborate and responsibilities are assigned.

Observer — Spring Events, @TransactionalEventListener, Kafka Strategy — Discount engine, Comparator, AuthenticationProvider Template Method — JdbcTemplate, AbstractSecurityInterceptor Command — Undo/redo, Kafka consumer, Spring Batch Step Chain of Responsibility — Spring Security filters, HandlerInterceptor State — Order state machine, Circuit Breaker, Spring StateMachine
6
Behavioral Patterns
30+
Code Snippets
25+
Interview Q&As
3/4
GoF Series
Day 13 · W2 Observer Pattern Behavioral
Intent

Define a one-to-many dependency between objects. When one object (subject/publisher) changes state, all registered observers are notified automatically. The subject and observers are loosely coupled — the subject doesn't know the concrete observer types.

Observer vs Pub-Sub — the interview distinction: In classic Observer, the subject holds direct references to observers and calls their update() method synchronously. In Pub-Sub, a message broker mediates — publisher and subscriber don't know about each other. Spring ApplicationEvents = Observer. Kafka = Pub-Sub.

Real-world trigger: An order is placed. Five things must happen: reserve inventory, charge payment, send confirmation email, award loyalty points, publish to Kafka. Without Observer: OrderService.createOrder() calls all five services — tightly coupled. With Observer: OrderService publishes ONE event. Five independent listeners react. Adding a sixth listener requires zero changes to OrderService.

JavaSpring ApplicationEvents — Observer in Production
// 1. DOMAIN EVENT — the state change notification (immutable data carrier)
public class OrderPlacedEvent extends ApplicationEvent {
    private final Order  order;
    private final String triggeredBy; // who triggered it (for audit)

    public OrderPlacedEvent(Object source, Order order, String user) {
        super(source);
        this.order       = order;
        this.triggeredBy = user;
    }
    public Order  getOrder()       { return order; }
    public String getTriggeredBy() { return triggeredBy; }
}

// 2. PUBLISHER / SUBJECT — publishes event, doesn't know who listens
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository       repo;
    private final ApplicationEventPublisher publisher;

    @Transactional
    public Order createOrder(CreateOrderRequest req, String userId) {
        Order order = repo.save(Order.from(req));
        // Single publish — no knowledge of who listens or what they do
        publisher.publishEvent(new OrderPlacedEvent(this, order, userId));
        return order;
        // Adding a new subscriber later = zero changes here ✅
    }
}

// 3. OBSERVERS — each handles one concern, zero coupling to each other

// Observer A: Inventory reservation — SYNCHRONOUS (same transaction)
@Component
public class InventoryReservationListener {
    private final InventoryService inventory;

    @EventListener  // synchronous, runs in SAME transaction as publisher
    public void onOrderPlaced(OrderPlacedEvent event) {
        inventory.reserve(event.getOrder().getItems());
        // If this throws, the whole transaction rolls back — intended!
    }
}

// Observer B: Email — ASYNCHRONOUS (don't fail order if email fails)
@Component
public class EmailNotificationListener {
    @EventListener
    @Async  // runs in separate thread pool — failure won't affect the order TX
    public void onOrderPlaced(OrderPlacedEvent event) {
        emailService.sendOrderConfirmation(event.getOrder());
    }
}

// Observer C: Kafka publisher — AFTER TRANSACTION COMMITS (critical!)
// @TransactionalEventListener prevents publishing to Kafka for a rolled-back order
@Component
public class KafkaOrderPublisher {
    private final KafkaTemplate<String, String> kafka;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onOrderPlaced(OrderPlacedEvent event) {
        // Only fires AFTER DB transaction committed — no phantom events!
        kafka.send("order.placed",
            event.getOrder().getId().toString(),
            objectMapper.writeValueAsString(event.getOrder()));
    }
}

// Observer D: Loyalty points — AFTER_ROLLBACK (give points on success only)
@Component
public class LoyaltyPointsListener {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onOrderPlaced(OrderPlacedEvent event) {
        loyaltyService.award(
            event.getOrder().getCustomerId(),
            event.getOrder().getTotal());
    }
}

// MANUAL OBSERVER (when not using Spring) — thread-safe with CopyOnWriteArrayList
public interface OrderObserver { void onOrderStateChange(Order order, OrderStatus newStatus); }

public class OrderSubject {
    // CopyOnWriteArrayList: safe for concurrent add/remove while iterating
    private final List<OrderObserver> observers = new CopyOnWriteArrayList<>();

    public void subscribe(OrderObserver o)   { observers.add(o); }
    public void unsubscribe(OrderObserver o) { observers.remove(o); }

    protected void notifyObservers(Order order, OrderStatus status) {
        observers.forEach(obs -> {
            try { obs.onOrderStateChange(order, status); }
            catch (Exception e) {
                log.error("Observer {} failed", obs.getClass().getSimpleName(), e);
                // Don't let one observer failure stop others
            }
        });
    }
}
🔩 Where Observer Is Used in Java Frameworks
Spring ApplicationEvent
ApplicationEventPublisher.publishEvent() + @EventListener. Built-in Observer. Synchronous by default; add @Async for async handling. Spring automatically discovers all @EventListener methods on context startup.
@TransactionalEventListener
Observer that fires at a specific transaction phase: AFTER_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION, BEFORE_COMMIT. Critical for preventing side effects on rolled-back transactions (Kafka publish, email send).
Spring WebSocket / STOMP
WebSocket clients are observers. Server publishes to /topic/orders; all subscribed clients receive updates. SimpMessagingTemplate.convertAndSend() is the publisher notification mechanism.
Hibernate Entity Listeners
@EntityListeners(AuditingEntityListener.class) — Hibernate calls @PrePersist, @PostPersist, @PostUpdate callbacks. Your audit listener observes entity lifecycle events.
Reactor / Project Reactor
Flux is a publisher; subscribe() registers observers. The entire reactive pipeline is Observer: flux.map().filter().subscribe(onNext, onError, onComplete).
Java 9 Flow API
Flow.Publisher, Flow.Subscriber, Flow.Subscription — Observer pattern with backpressure. SubmissionPublisher is the built-in concrete publisher. Reactive Streams spec built on Observer.
🎯 Interview Questions — Observer
Observer vs Pub-Sub — what is the architectural difference? When to use Kafka vs Spring Events?
Observer (tight coupling): Subject holds direct references to observers. Same JVM process. Synchronous by default. Subject calls observers directly — knows the observer list. Example: Spring ApplicationEvents.
Pub-Sub (loose coupling): Broker (Kafka, RabbitMQ) mediates. Publisher and subscriber don't know about each other. Cross-process, cross-service. Messages are durable and replayable. Asynchronous.
When to use Spring Events: In-process coordination within one service (inventory reservation + audit log must succeed/fail together with the order).
When to use Kafka: Different microservices need to react. Consumers can be offline. Need replay/durability. Eventual consistency acceptable. Example: Order service publishes to Kafka; 3 downstream services (email, analytics, warehouse) consume independently at their own pace.
Why use @TransactionalEventListener instead of @EventListener? What bug does it prevent?
The bug without @TransactionalEventListener: With plain @EventListener, the observer runs during the transaction. If the transaction later rolls back (e.g., payment fails in a downstream service call), the order is rolled back from DB — but the email was already sent and the Kafka message was already published. Customer gets a confirmation email for a non-existent order.
@TransactionalEventListener AFTER_COMMIT: The observer fires only after the DB transaction commits successfully. No more phantom emails or Kafka messages for rolled-back orders.
Caveat: The observer runs outside the original transaction. If you need transactional behavior in the observer, annotate the observer method with @Transactional(propagation = REQUIRES_NEW) to create a new transaction.
How does Observer enable the Open/Closed Principle in event-driven architecture?
Without Observer: OrderService.createOrder() explicitly calls email, inventory, loyalty, analytics, shipping services. Adding a new consumer = modify OrderService — violates OCP.
With Observer: OrderService publishes ONE event and has NO dependencies on consumers. Adding a WhatsApp notification listener = create new @Component with @EventListener. Zero changes to OrderService. This is OCP: open for extension (new listeners), closed for modification (OrderService unchanged). This is the architectural advantage of event-driven microservices — producers and consumers evolve independently.
How do you handle observer failures? What if one observer throws an exception?
Synchronous @EventListener: If the observer throws, the exception propagates to the publisher. If the publisher is @Transactional, the transaction rolls back. This is sometimes intentional (inventory reservation failure should roll back the order). Use try-catch in the observer for non-critical side effects.
@Async listeners: Exceptions are swallowed unless you configure an AsyncUncaughtExceptionHandler. Configure with @EnableAsync + implement AsyncConfigurer.getAsyncUncaughtExceptionHandler().
Manual observer pattern: Always wrap each observer call in try-catch and log failures without stopping the notification loop — otherwise one bad observer silently prevents all subsequent observers from being notified.
Day 14 · W2 Strategy Pattern Behavioral
Intent

Define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. The context selects which strategy (algorithm) to use without changing its own code. Also called the Policy pattern.

Java 8+ insight: Strategy IS a functional interface. Any @FunctionalInterface with one abstract method is a Strategy. Comparator, Predicate, Function, Runnable are all Strategy interfaces. Lambdas are inline Strategy implementations. This is why Strategy is the most natural pattern in modern Java.

Strategy vs State: Strategy = the CLIENT chooses which algorithm to use from outside. State = the OBJECT changes its own behavior based on internal state transitions. Strategy variants are independent; State transitions are coordinated.

JavaDiscount Strategy Engine + Java 8 Lambda Strategies + Spring Security
// STRATEGY INTERFACE — defines the algorithm contract
public interface DiscountStrategy {
    BigDecimal apply(BigDecimal originalPrice, Cart cart, Customer customer);
    boolean    isApplicable(Cart cart, Customer customer);
    String     getStrategyName();
    default int getPriority() { return 100; } // lower = higher priority
}

// CONCRETE STRATEGIES
@Component
public class SeasonalDiscountStrategy implements DiscountStrategy {
    @Override
    public BigDecimal apply(BigDecimal price, Cart cart, Customer c) {
        return price.multiply(BigDecimal.valueOf(0.80)); // 20% off
    }
    @Override
    public boolean isApplicable(Cart cart, Customer c) {
        Month now = LocalDate.now().getMonth();
        return now == Month.NOVEMBER || now == Month.DECEMBER; // festive season
    }
    public String getStrategyName() { return "SEASONAL_20PCT"; }
    public int    getPriority()      { return 10; } // high priority
}

@Component
public class LoyaltyDiscountStrategy implements DiscountStrategy {
    @Override
    public BigDecimal apply(BigDecimal price, Cart cart, Customer c) {
        int pts = c.getLoyaltyPoints();
        double pct = pts > 5000 ? 0.15 : pts > 1000 ? 0.10 : 0.05;
        return price.multiply(BigDecimal.valueOf(1 - pct));
    }
    @Override
    public boolean isApplicable(Cart cart, Customer c) {
        return c.getLoyaltyPoints() >= 500;
    }
    public String getStrategyName() { return "LOYALTY_TIERED"; }
}

@Component
public class BulkDiscountStrategy implements DiscountStrategy {
    @Override
    public BigDecimal apply(BigDecimal price, Cart cart, Customer c) {
        int qty = cart.getTotalQuantity();
        if (qty >= 20) return price.multiply(BigDecimal.valueOf(0.75));
        if (qty >= 10) return price.multiply(BigDecimal.valueOf(0.85));
        return price.multiply(BigDecimal.valueOf(0.90));
    }
    @Override
    public boolean isApplicable(Cart cart, Customer c) {
        return cart.getTotalQuantity() >= 5;
    }
    public String getStrategyName() { return "BULK_TIERED"; }
}

// CONTEXT — selects and applies the best strategy
@Service
public class PricingService {
    private final List<DiscountStrategy> strategies; // Spring injects all @Components

    public PricingResult calculatePrice(Cart cart, Customer customer) {
        BigDecimal originalPrice = cart.getSubtotal();

        // Find best applicable strategy (lowest price wins)
        Optional<DiscountStrategy> bestStrategy = strategies.stream()
            .filter(s -> s.isApplicable(cart, customer))
            .min(Comparator.comparingInt(DiscountStrategy::getPriority));

        return bestStrategy.map(s -> {
            BigDecimal discounted = s.apply(originalPrice, cart, customer);
            log.info("Applied strategy: {} | {} → {}", s.getStrategyName(), originalPrice, discounted);
            return PricingResult.of(discounted, s.getStrategyName());
        }).orElse(PricingResult.noDiscount(originalPrice));
    }
}

// JAVA 8+ LAMBDA STRATEGIES — Strategy IS a Functional Interface
// Comparator is Strategy — sorts are algorithm variants
List<Order> orders = getOrders();
orders.sort(Comparator.comparing(Order::getTotal).reversed()); // strategy: sort by total desc
orders.sort(Comparator.comparing(Order::getCreatedAt));           // strategy: sort by date

// Predicate is Strategy — filtering algorithm
Predicate<Order> highValue  = o -> o.getTotal().compareTo(BigDecimal.valueOf(10000)) > 0;
Predicate<Order> recentOnly = o -> o.getCreatedAt().isAfter(LocalDate.now().minusDays(7));
orders.stream().filter(highValue.and(recentOnly)).toList();

// Spring Security: AuthenticationProvider IS Strategy
// ProviderManager tries each registered AuthenticationProvider in order
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
    private final JwtService jwtService;
    @Override
    public Authentication authenticate(Authentication auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        return jwtService.validateAndCreate(token); // or throw BadCredentialsException
    }
    @Override
    public boolean supports(Class<?> authType) { return authType == JwtAuthToken.class; }
}

@Component
public class ApiKeyAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication auth) throws AuthenticationException {
        // validate API key — different strategy for different auth type
        return apiKeyService.validateAndCreate((String) auth.getCredentials());
    }
    @Override
    public boolean supports(Class<?> authType) { return authType == ApiKeyAuthToken.class; }
}
🔩 Where Strategy Is Used in Java Frameworks
Collections.sort / Comparator
Comparator is the Strategy interface for sorting algorithms. Collections.sort(list, comparator) accepts different comparison strategies. Every lambda you pass to sorted() is a Strategy implementation.
Spring Security AuthenticationProvider
ProviderManager is the context. It iterates registered AuthenticationProvider strategies and uses the first one that supports() the authentication type. JWT, LDAP, DB — each is a separate strategy.
Spring RetryPolicy (Spring Retry)
RetryPolicy strategy determines whether to retry. SimpleRetryPolicy, ExceptionClassifierRetryPolicy, TimeoutRetryPolicy are concrete strategies swapped without changing the retry template.
Hibernate FlushMode / CacheMode
FlushMode (AUTO, COMMIT, ALWAYS, MANUAL) is a strategy for when to flush the session to DB. Different strategies per use case — bulk operations use MANUAL to avoid per-entity flushes.
Jackson ObjectMapper — SerializationFeature
Jackson serializers and deserializers are strategies. JsonSerializer<T> defines the serialization algorithm. Swap date formats, null handling, field naming — all via Strategy without touching domain classes.
Spring HandlerMethodArgumentResolver
Each resolver is a Strategy for extracting a specific method parameter type. RequestParamMethodArgumentResolver handles @RequestParam; PathVariableMethodArgumentResolver handles @PathVariable.
🎯 Interview Questions — Strategy
Strategy vs State vs Template Method — the definitive comparison.
Strategy: Client EXTERNALLY selects the algorithm. Strategies are independent, interchangeable. Context holds current strategy but doesn't transition between them — client does. Focus: what algorithm to use. Example: discount calculation (client selects seasonal vs loyalty vs bulk).
State: Object INTERNALLY manages its own behavior change based on state transitions. States know about each other (PendingState knows to go to PaidState). Client sees same interface but gets different behavior. Focus: behavior changes as state changes. Example: order status machine (PENDING→PAID→SHIPPED).
Template Method: Parent class defines the algorithm skeleton. Subclasses fill in specific steps via overriding. Algorithm STRUCTURE is fixed; details vary. Inheritance-based. Focus: invariant algorithm with variable steps. Example: JdbcTemplate (framework handles connection, you provide SQL + RowMapper).
How does Java 8+ make Strategy pattern natural? Give 3 examples.
Any @FunctionalInterface is a Strategy interface. Any lambda is an inline Strategy implementation. This eliminates the boilerplate of creating named strategy classes for simple cases:
1. Comparator: orders.sort((a, b) -> a.getTotal().compareTo(b.getTotal())) — lambda IS the sorting strategy, passed externally.
2. Predicate: customers.stream().filter(c -> c.getTier() == GOLD) — lambda IS the filtering strategy.
3. RetryPolicy via lambda: RetryTemplate.builder().retryOn(IOException.class).maxAttempts(3).build() — configureable strategy without subclassing.
Before Java 8: every strategy required a named class implementing the interface. Lambdas make Strategy nearly zero-cost syntactically.
How does Spring Security's ProviderManager implement Strategy? How does it select the right one?
ProviderManager holds a List<AuthenticationProvider>. When authenticate(Authentication) is called: (1) Iterate each provider. (2) Call provider.supports(authentication.getClass()). (3) If supported, call provider.authenticate(auth). (4) If it returns non-null, use the result. (5) If it throws AuthenticationException, try next. (6) If no provider succeeds, throw ProviderNotFoundException.
This is the Chain of Responsibility + Strategy combination. Each provider is a Strategy; the manager chains them. Register custom providers via httpSecurity.authenticationProvider(myProvider).
What is the Open/Closed Principle violation that Strategy solves?
Without Strategy: PricingService.calculatePrice() has a switch/if-else:
if (isSeasonal) return price * 0.8; else if (isLoyalCustomer) return price * 0.9; else if (isBulk) return...
Adding a "flash sale" discount requires MODIFYING PricingService — violates OCP (closed for modification).
With Strategy: Adding flash sale = create one new @Component FlashSaleDiscountStrategy. Spring auto-discovers it. PricingService is untouched. Open for extension (new strategies), closed for modification (PricingService unchanged). The pattern enforces OCP at the algorithm level.
Day 15 · W3 Template Method Pattern Behavioral
Intent

Define the skeleton of an algorithm in a base class, deferring some steps to subclasses via abstract or hook methods. The template method is declared final — subclasses fill in variable steps without changing the algorithm structure.

When to reach for it: Multiple classes share the same algorithm outline (open connection, execute query, close connection) but differ in specific steps (what SQL to run, how to map results). Extract the invariant skeleton to an abstract class; subclasses provide the variant steps.

Hook methods vs abstract methods: Abstract methods MUST be overridden by subclasses (required steps). Hook methods have a default empty/passthrough implementation — subclasses override them OPTIONALLY (extension points). Good template classes use both strategically.

JavaData Export Pipeline + JdbcTemplate Internals
// TEMPLATE: abstract base class defines the algorithm skeleton
public abstract class DataExporter<T> {

    // THE TEMPLATE METHOD — final: cannot be overridden, algorithm structure is locked
    public final ExportResult export(ExportRequest request) {
        log.info("Starting export: type={}", getExportType());

        validate(request);                          // step 1: fixed (provided by base class)
        List<T> raw      = fetchData(request);      // step 2: abstract — subclass must provide
        List<T> filtered = applyFilters(raw);       // step 3: hook — optional override
        String   output   = format(filtered);        // step 4: abstract — subclass must provide
        ExportResult result = writeOutput(output, request); // step 5: fixed
        onExportComplete(result);                    // step 6: hook — optional override

        log.info("Export complete: rows={} file={}", result.getRowCount(), result.getFilename());
        return result;
    }

    // ABSTRACT steps: subclasses MUST implement
    protected abstract List<T> fetchData(ExportRequest request);
    protected abstract String    format(List<T> data);
    protected abstract String    getExportType();

    // HOOK steps: optional overrides (default = no-op or passthrough)
    protected List<T> applyFilters(List<T> data) { return data; } // passthrough by default
    protected void    onExportComplete(ExportResult r) { }        // no-op by default

    // FIXED steps: fully implemented in base class, shared by all subclasses
    private void validate(ExportRequest req) {
        Objects.requireNonNull(req, "Request cannot be null");
        if (req.getFromDate().isAfter(req.getToDate())) {
            throw new IllegalArgumentException("fromDate must be before toDate");
        }
    }
    private ExportResult writeOutput(String content, ExportRequest req) {
        String filename = req.getBaseFilename() + "_" + Instant.now().getEpochSecond();
        s3Client.putObject(bucket, filename, content);
        return ExportResult.of(filename, content.split("\n").length);
    }
}

// CONCRETE 1: Order CSV exporter
@Service
public class OrderCsvExporter extends DataExporter<Order> {
    private final OrderRepository repo;

    @Override
    protected List<Order> fetchData(ExportRequest req) {
        return repo.findByDateRange(req.getFromDate(), req.getToDate());
    }
    @Override
    protected String format(List<Order> orders) {
        var sb = new StringBuilder("id,customerId,status,total,createdAt\n");
        orders.forEach(o -> sb.append(
            String.format("%s,%s,%s,%.2f,%s\n", o.getId(), o.getCustomerId(),
                o.getStatus(), o.getTotal(), o.getCreatedAt())));
        return sb.toString();
    }
    @Override
    protected List<Order> applyFilters(List<Order> data) {
        return data.stream().filter(o -> o.getStatus() != OrderStatus.CANCELLED).toList();
    }
    @Override
    protected void onExportComplete(ExportResult r) {
        auditLog.record("ORDER_EXPORT", r.getFilename(), r.getRowCount());
    }
    protected String getExportType() { return "ORDER_CSV"; }
}

// CONCRETE 2: Financial report PDF exporter — different steps, same template
@Service
public class FinancialPdfExporter extends DataExporter<Transaction> {
    @Override
    protected List<Transaction> fetchData(ExportRequest req) {
        return txRepo.findByPeriod(req.getFromDate(), req.getToDate());
    }
    @Override
    protected String format(List<Transaction> txs) {
        return pdfGenerator.generate(txs); // totally different formatting
    }
    protected String getExportType() { return "FINANCIAL_PDF"; }
    // applyFilters and onExportComplete use default (passthrough / no-op)
}

// HOW SPRING's JdbcTemplate IS Template Method:
// The template method: JdbcTemplate.query(sql, RowMapper, args...)
// FIXED steps (handled by JdbcTemplate):
//   → getConnection() from pool
//   → create PreparedStatement
//   → set parameters
//   → execute query
//   → translate SQLException to DataAccessException
//   → close resources (finally block)
// YOUR variable steps:
//   → the SQL string
//   → the RowMapper (how to turn each row into a Java object)

@Repository
public class OrderJdbcRepository {
    private final JdbcTemplate jdbc;

    public List findHighValueOrders(BigDecimal minAmount) {
        return jdbc.query(                                    // ← template method
            "SELECT id, total, status FROM orders WHERE total >= ?",   // YOUR SQL
            (rs, rowNum) -> new OrderSummary(             // YOUR RowMapper
                rs.getObject("id", UUID.class),
                rs.getBigDecimal("total"),
                OrderStatus.valueOf(rs.getString("status"))
            ),
            minAmount                                        // YOUR parameters
        );
        // JdbcTemplate handles everything else — open, prepare, execute, close
    }
}
🔩 Where Template Method Is Used in Java Frameworks
Spring JdbcTemplate
query(sql, RowMapper, args) is the template. Spring handles connection lifecycle, error translation, resource cleanup. You provide SQL and RowMapper — the variable steps.
Spring AbstractController
handleRequest() is the template. It calls checkAndPrepare(), then handleRequestInternal() — your abstract method. Session checks, locale handling, method routing are the fixed steps.
Spring Batch ItemReader/Processor/Writer
Spring Batch's chunk processing is a template: read → process → write. The Step framework manages transactions, skip logic, retry — you provide the three implementations. AbstractItemStreamItemReader has doRead() as the abstract step.
Spring Security AbstractSecurityInterceptor
Template for all security interceptors. Fixed steps: obtain security object, get ConfigAttributes, call AccessDecisionManager. Abstract method getSecureObjectClass() varies by subclass (method security vs URL security).
Hibernate AbstractEntityPersister
Template for entity persistence: generate SQL, bind parameters, execute, extract results. Subclasses (SingleTableEntityPersister, JoinedSubclassEntityPersister) provide SQL generation strategies for different inheritance mappings.
Spring RestTemplate (deprecated) / WebClient
RestTemplate.execute() is a template: create request, set headers, write body, send, extract response. You provide RequestCallback and ResponseExtractor — the variable steps in the fixed HTTP request/response lifecycle.
🎯 Interview Questions — Template Method
How does JdbcTemplate use Template Method? Walk through what happens when you call query().
When you call jdbcTemplate.query(sql, rowMapper, args):
  1. JdbcTemplate gets a Connection from the DataSource pool (FIXED)
  2. Creates a PreparedStatement using your SQL (YOUR variable step)
  3. Calls ArgumentPreparedStatementSetter.setValues(ps) with your args (FIXED)
  4. Executes the statement (FIXED)
  5. Iterates the ResultSet — calls your RowMapper.mapRow(rs, rowNum) for each row (YOUR variable step)
  6. Wraps any SQLException into Spring's DataAccessException hierarchy (FIXED)
  7. Closes PreparedStatement and returns Connection to pool in finally (FIXED)
You never write steps 1, 3, 4, 6, 7. Template Method eliminates the boilerplate that every JDBC call requires.
Template Method vs Strategy — both vary an algorithm. What's the key difference?
Template Method uses inheritance: The algorithm skeleton is in the abstract class. Variable steps are methods overridden by subclasses. You create a new class to vary behavior. The relationship is compile-time and fixed. Once you subclass DataExporter, the template is fixed for that class.
Strategy uses composition: The algorithm is encapsulated in a separate object passed to the context. You pass a different object at runtime to change behavior. No inheritance required. More flexible — you can swap strategies on the same object at runtime.
Rule of thumb: Template Method for framework extension points (you write code that the framework calls). Strategy for runtime behavior selection (you pick which algorithm to use when calling). Template Method is framework-designer's tool; Strategy is application-developer's tool.
Why should the template method be declared final?
The template method defines the invariant algorithm structure — it's the contract the framework provides. If subclasses can override the template method, they can break the contract: skip validation, change the order of steps, or bypass resource cleanup. Declaring it final enforces the Hollywood Principle: "Don't call us, we'll call you." The framework (parent) controls the flow; subclasses (you) provide the content. JdbcTemplate, Spring Batch, and other Spring templates all follow this pattern — they control the lifecycle, you control the domain logic within designated extension points.
Day 16 · W3 Command Pattern Behavioral
Intent

Encapsulate a request as an object. This lets you parameterize methods with different requests, queue or log requests, and implement undoable operations. A command object contains everything needed to execute the action — the receiver, the method, and arguments.

Four key participants: (1) Command — interface with execute(). (2) ConcreteCommand — holds receiver + does the work. (3) Invoker — calls execute(), manages history queue. (4) Receiver — the actual business object (OrderService, Repository).

Real-world uses: Undo/redo in editors. Async job queues (Kafka, RabbitMQ messages ARE commands). Spring Batch Step = Command. Transactional outbox pattern (store command in DB, execute later).

JavaCart Operations with Undo/Redo + Async Command Queue via Kafka
// COMMAND INTERFACE
public interface CartCommand {
    void    execute();
    void    undo();
    String describe();
    boolean isUndoable();
}

// CONCRETE COMMAND 1: Add item to cart
public class AddItemCommand implements CartCommand {
    private final Cart           cart;
    private final OrderItem      item;
    private final CartRepository repo;

    @Override
    public void execute() {
        cart.addItem(item);
        repo.save(cart);
    }
    @Override
    public void undo() {
        cart.removeItem(item.getProductId());
        repo.save(cart);
    }
    public  String  describe()   { return "Add " + item.getName() + " × " + item.getQty(); }
    public boolean isUndoable() { return true; }
}

// CONCRETE COMMAND 2: Apply coupon
public class ApplyCouponCommand implements CartCommand {
    private final Cart           cart;
    private final String         couponCode;
    private final CartRepository repo;
    private       String         previousCoupon; // saved for undo

    @Override
    public void execute() {
        previousCoupon = cart.getCouponCode(); // save state for undo
        cart.applyCoupon(couponCode);
        repo.save(cart);
    }
    @Override
    public void undo() {
        cart.applyCoupon(previousCoupon); // restore previous state
        repo.save(cart);
    }
    public  String  describe()   { return "Apply coupon: " + couponCode; }
    public boolean isUndoable() { return true; }
}

// INVOKER — manages undo/redo history stacks
@Service
public class CartCommandInvoker {
    private final Deque<CartCommand> undoStack = new ArrayDeque<>();
    private final Deque<CartCommand> redoStack = new ArrayDeque<>();
    private static final int MAX_HISTORY = 50;

    public void execute(CartCommand cmd) {
        cmd.execute();
        if (cmd.isUndoable()) {
            undoStack.push(cmd);
            redoStack.clear();                      // new command invalidates redo history
            if (undoStack.size() > MAX_HISTORY) undoStack.removeLast(); // cap history
        }
    }

    public Optional<String> undo() {
        if (undoStack.isEmpty()) return Optional.empty();
        CartCommand cmd = undoStack.pop();
        cmd.undo();
        redoStack.push(cmd);
        return Optional.of("Undone: " + cmd.describe());
    }

    public Optional<String> redo() {
        if (redoStack.isEmpty()) return Optional.empty();
        CartCommand cmd = redoStack.pop();
        cmd.execute();
        undoStack.push(cmd);
        return Optional.of("Redone: " + cmd.describe());
    }

    public List<String> getHistory() {
        return undoStack.stream().map(CartCommand::describe).toList();
    }
}

// KAFKA AS COMMAND QUEUE: Serialize commands and consume asynchronously
public record PlaceOrderCommand(UUID orderId, UUID customerId,
                                  List<OrderItem> items, PaymentMethod paymentMethod) {}

@Service
public class OrderCommandGateway {
    private final KafkaTemplate<String, String> kafka;

    public void send(PlaceOrderCommand cmd) {
        // Command is serialized and enqueued — caller is decoupled from execution
        kafka.send("order.commands", cmd.orderId().toString(),
            objectMapper.writeValueAsString(cmd));
    }
}

@KafkaListener(topics = "order.commands", groupId = "order-processors")
public void processCommand(ConsumerRecord<String, String> record) {
    PlaceOrderCommand cmd = objectMapper.readValue(record.value(), PlaceOrderCommand.class);
    orderService.placeOrder(cmd); // Kafka guarantees at-least-once execution
}
🔩 Where Command Is Used in Java Frameworks
Spring Batch — Step
Each Step is a Command: execute(StepExecution). The Job is the Invoker — it queues and executes Steps in order. Steps can be retried and restarted — Command enables these capabilities.
Kafka / RabbitMQ Messages
Every message IS a Command: serialized request with all data needed to execute. The message queue is the Invoker's buffer. Consumers are receivers. Enables decoupling, durable queuing, and async execution.
Spring @Transactional Rollback
Transaction log = Command log. Each SQL statement is recorded. On rollback: execute undo operations in reverse order. The DB transaction manager IS a Command Invoker with undo capability.
Runnable / Callable
Runnable and Callable are Command interfaces. ExecutorService.submit(runnable) queues the command for async execution. ScheduledExecutorService is an Invoker that executes commands on a schedule.
Spring WebFlux — Mono/Flux operators
Each operator (map, filter, flatMap) is a deferred Command added to the pipeline. Nothing executes until subscribe() is called — the Invoker. The entire reactive pipeline is a Command queue.
🎯 Interview Questions — Command
How would you implement undo/redo in a Java application?
  1. Define Command interface with execute() and undo().
  2. In execute(): capture previous state in the command object before applying the change.
  3. Invoker maintains two Deques: undoStack and redoStack (use ArrayDeque, not Stack — Stack is synchronized and slow).
  4. On execute(cmd): call cmd.execute(), push to undoStack, clear redoStack.
  5. On undo(): pop from undoStack, call cmd.undo(), push to redoStack.
  6. On redo(): pop from redoStack, call cmd.execute(), push to undoStack.
  7. Cap history with MAX_HISTORY to prevent memory leaks.
Key design choice: save the previous state IN the command (not in a separate Memento) when the state is small. Use Memento pattern when the saved state is complex or large.
Command vs Strategy — they both encapsulate behavior. What's different?
Strategy: Encapsulates how to do something — an algorithm variant. Stateless (usually). The context picks which strategy to use for ALL future calls. Focus: interchangeable algorithms.
Command: Encapsulates what to do and when — a complete request. Stateful (stores previous state for undo). Each instance represents ONE specific operation. Focus: request queuing, undo/redo, audit logging.
Structural difference: Strategy has execute(args) — stateless algorithm. Command has execute() — stateful, all args captured at construction time. Strategy objects are shared and reused; Command objects are single-use.
Example: DiscountStrategy = Strategy (reused for every cart). AddItemCommand = Command (one instance per specific add-item action, stores the item for undo).
How do Kafka messages implement the Command pattern?
A Kafka message body is a serialized Command — it contains all the information needed to execute an operation: order ID, customer ID, items, payment method. The producer is the Command creator. Kafka topic is the Command queue (Invoker's buffer). Consumers are Receivers that execute the command.
Advantages of Command-via-Kafka: (1) Durability — commands persist even if consumers are down. (2) Replay — rewind offsets to reprocess commands. (3) Audit log — messages are an immutable log of all commands ever received. (4) Decoupling — producer doesn't know which consumer will handle it or when.
Idempotency is critical: Kafka guarantees at-least-once delivery. Commands may execute more than once. Design your receiver to be idempotent — check if order already exists before creating it.
Day 17 · W3 Chain of Responsibility Behavioral
Intent

Pass a request along a chain of handlers. Each handler decides to process and stop, process and continue, or skip and continue. Decouples sender from receivers; multiple handlers can process a request without the sender knowing how many or which ones.

CoR vs Decorator: In Decorator, EVERY decorator always executes and delegates — the chain always reaches the core. In CoR, a handler MAY short-circuit the chain — it either handles the request completely OR passes it on. Spring Security's filter chain is CoR because a filter can set the response directly and stop the chain (returning 401) without calling chain.doFilter().

JavaAPI Request Processing Chain + Spring Security Filter Chain
// HANDLER INTERFACE
public abstract class RequestHandler {
    protected RequestHandler next;

    public RequestHandler setNext(RequestHandler next) {
        this.next = next;
        return next; // return next for fluent chaining: a.setNext(b).setNext(c)
    }

    public abstract void handle(ApiRequest req, ApiResponse resp);

    protected void passToNext(ApiRequest req, ApiResponse resp) {
        if (next != null) next.handle(req, resp);
    }
}

// HANDLER 1: Auth — short-circuits if unauthenticated
public class AuthenticationHandler extends RequestHandler {
    @Override
    public void handle(ApiRequest req, ApiResponse resp) {
        String token = req.getHeader("Authorization");
        if (token == null || !jwtService.isValid(token)) {
            resp.setStatus(401); resp.setBody("Unauthorized");
            return; // SHORT-CIRCUIT — stops the chain
        }
        req.setAttribute("user", jwtService.extractUser(token));
        passToNext(req, resp); // pass to next handler
    }
}

// HANDLER 2: Rate limiting
public class RateLimitHandler extends RequestHandler {
    @Override
    public void handle(ApiRequest req, ApiResponse resp) {
        String userId = (String) req.getAttribute("user");
        if (!rateLimiter.tryConsume(userId)) {
            resp.setStatus(429); resp.setHeader("Retry-After", "60");
            return; // SHORT-CIRCUIT — stops chain
        }
        passToNext(req, resp);
    }
}

// HANDLER 3: Authorization
public class AuthorizationHandler extends RequestHandler {
    @Override
    public void handle(ApiRequest req, ApiResponse resp) {
        String user = (String) req.getAttribute("user");
        if (!accessControl.hasPermission(user, req.getPath(), req.getMethod())) {
            resp.setStatus(403); resp.setBody("Forbidden");
            return; // SHORT-CIRCUIT
        }
        passToNext(req, resp);
    }
}

// HANDLER 4: Business logic — terminal handler
public class BusinessLogicHandler extends RequestHandler {
    @Override
    public void handle(ApiRequest req, ApiResponse resp) {
        Object result = orderService.processRequest(req);
        resp.setStatus(200);
        resp.setBody(objectMapper.writeValueAsString(result));
        // No passToNext — this is the final handler
    }
}

// BUILD AND USE the chain — fluent linking
RequestHandler auth = new AuthenticationHandler();
auth.setNext(new RateLimitHandler())
    .setNext(new AuthorizationHandler())
    .setNext(new BusinessLogicHandler());

auth.handle(request, response); // start chain at head

// SPRING: OncePerRequestFilter IS your CoR handler
@Component
@Order(1) // position in filter chain
public class CorrelationIdFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                       HttpServletResponse res,
                                       FilterChain chain)
                                       throws IOException, ServletException {
        String id = Optional.ofNullable(req.getHeader("X-Request-ID"))
                             .orElse(UUID.randomUUID().toString());
        MDC.put("requestId", id);
        res.setHeader("X-Request-ID", id);
        try {
            chain.doFilter(req, res); // PASS to next handler — CoR continue
        } finally {
            MDC.clear(); // cleanup after entire chain completes
        }
    }
}
🔩 Where Chain of Responsibility Is Used in Java Frameworks
Spring Security FilterChainProxy
15+ ordered filters in the chain: CSRF, session, authentication, authorization. Each filter either sets response (short-circuit) or calls chain.doFilter() to pass on. Ordering matters — authentication must run before authorization.
Spring MVC HandlerInterceptor
preHandle() returns boolean — false stops the chain, true continues. Multiple interceptors are chained in registration order. Used for auth checks, request logging, locale resolution before hitting the controller.
Java try-catch blocks
Multiple catch blocks form a chain. Each block handles specific exception types. If a catch block can't handle the exception, it propagates to the next (catch for supertype or the method caller). This is textbook CoR.
Spring Cloud Gateway — GatewayFilter
Filters chain around request/response: authentication → rate limiting → circuit breaker → routing → response modification. Each filter calls chain.filter(exchange) to continue or returns early to short-circuit.
Servlet Filter Chain (javax.servlet)
The original Java CoR. FilterChain.doFilter() passes to the next filter. Each filter wraps request and response processing. The servlet is the terminal handler at the end of the chain.
Spring ExceptionHandlerExceptionResolver
Multiple exception resolvers form a chain: ExceptionHandlerExceptionResolverResponseStatusExceptionResolverDefaultHandlerExceptionResolver. Each tries to resolve the exception; the first that succeeds stops the chain.
🎯 Interview Questions — Chain of Responsibility
How does Spring Security's filter chain work? What is FilterChainProxy?
DelegatingFilterProxy is registered with the Servlet container. It delegates to FilterChainProxy, which is a Spring bean. FilterChainProxy holds a list of SecurityFilterChain objects — each matched by request pattern (e.g., /api/** vs /admin/**). The matching chain's filters are executed in order.
Key filters in order:
1. DisableEncodeUrlFilter → 2. SecurityContextHolderFilter (loads auth from session) → 3. HeaderWriterFilter (HSTS, XSS headers) → 4. CsrfFilter → 5. UsernamePasswordAuthenticationFilter → 6. BearerTokenAuthenticationFilter (JWT) → 7. ExceptionTranslationFilter (converts auth exceptions to 401/403) → 8. AuthorizationFilter (enforces @PreAuthorize rules).
Ordering matters: you cannot authorize before authenticating.
CoR vs Decorator — both chain objects of the same interface. What's the key difference?
Decorator: EVERY decorator always executes and delegates to the next. The chain always reaches the core component. Adding behavior around the core — all layers run. Example: Java I/O streams — BufferedInputStream always delegates to the wrapped InputStream.
Chain of Responsibility: A handler MAY short-circuit the chain — it either handles the request COMPLETELY (sets response, returns) OR passes it on. Not every handler always runs. Example: Spring Security filter — if AuthenticationFilter decides 401, it returns immediately without calling the next filter.
Structural test: Does EVERY object in the chain always execute? → Decorator. Can execution stop midway? → CoR.
How do you add a custom Spring Security filter? How do you control its position?
Extend OncePerRequestFilter and annotate with @Component. Register it via HttpSecurity.addFilterBefore(), addFilterAfter(), or addFilterAt():
http.addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class)
This inserts your filter BEFORE the standard auth filter. Your filter authenticates via JWT; if successful, sets SecurityContextHolder — the standard filter then finds an existing auth and skips.
Use @Order for filters registered as separate Spring beans. For security filters specifically, always use addFilterBefore/After to position relative to known security filters — @Order alone doesn't control position within the security filter chain.
Day 18 · W3 State Pattern Behavioral
Intent

Allow an object to alter its behavior when its internal state changes. The object appears to change its class. State transitions are encapsulated in state objects — no giant switch/if-else checking state everywhere in the codebase.

The problem it solves: Without State pattern, you scatter if (status == PENDING) ... else if (status == CONFIRMED) ... checks throughout your code. Add a new status → modify every check. With State: each state is a class. Adding a new status = add one class. Existing classes untouched.

Key difference from Strategy: Strategy is chosen by the client from outside. State transitions happen INTERNALLY — the current state decides the next state based on events. States know about each other (PendingState transitions to ConfirmedState); strategies are independent.

JavaOrder State Machine + Resilience4j Circuit Breaker States
// STATE INTERFACE — all operations the order can receive
public interface OrderState {
    void   confirm(OrderContext ctx);
    void   pay(OrderContext ctx, String transactionId);
    void   ship(OrderContext ctx, String trackingCode);
    void   deliver(OrderContext ctx);
    void   cancel(OrderContext ctx, String reason);
    String getStatusName();
}

// CONTEXT — holds current state, delegates all operations to it
@Entity
public class OrderContext {
    @Id private UUID id;
    @Transient                       // state is not persisted — reconstructed from statusName
    private OrderState state;
    private String statusName = "DRAFT";

    public void setState(OrderState newState) {
        log.info("Order {} state: {} → {}", id, statusName, newState.getStatusName());
        this.state = newState;
        this.statusName = newState.getStatusName(); // persisted field
    }

    // All public methods delegate to the current state
    public void confirm()                        { state.confirm(this); }
    public void pay(String txId)                { state.pay(this, txId); }
    public void ship(String trackingCode)       { state.ship(this, trackingCode); }
    public void deliver()                        { state.deliver(this); }
    public void cancel(String reason)           { state.cancel(this, reason); }
    public String getStatusName()               { return statusName; }
}

// CONCRETE STATE: DRAFT — starting state
public class DraftState implements OrderState {
    @Override
    public void confirm(OrderContext ctx) {
        ctx.setState(new ConfirmedState()); // valid transition: DRAFT → CONFIRMED
    }
    @Override
    public void cancel(OrderContext ctx, String reason) {
        ctx.setState(new CancelledState(reason));
    }
    @Override
    public void pay(OrderContext ctx, String txId) {
        throw new InvalidOrderStateException("Cannot pay DRAFT order — confirm it first");
    }
    @Override
    public void ship(OrderContext ctx, String code) {
        throw new InvalidOrderStateException("Cannot ship DRAFT order");
    }
    @Override public void deliver(OrderContext ctx) { throw new InvalidOrderStateException("Cannot deliver DRAFT"); }
    public String getStatusName() { return "DRAFT"; }
}

// CONCRETE STATE: CONFIRMED
public class ConfirmedState implements OrderState {
    @Override
    public void pay(OrderContext ctx, String txId) {
        ctx.setState(new PaidState(txId)); // CONFIRMED → PAID
    }
    @Override
    public void cancel(OrderContext ctx, String r) { ctx.setState(new CancelledState(r)); }
    @Override
    public void confirm(OrderContext ctx)          { throw new InvalidOrderStateException("Already confirmed"); }
    @Override
    public void ship(OrderContext ctx, String c)   { throw new InvalidOrderStateException("Not paid yet"); }
    @Override public void deliver(OrderContext c)   { throw new InvalidOrderStateException("Not shipped"); }
    public String getStatusName() { return "CONFIRMED"; }
}

// CONCRETE STATE: PAID
public class PaidState implements OrderState {
    private final String transactionId;
    public PaidState(String txId) { this.transactionId = txId; }
    @Override
    public void ship(OrderContext ctx, String code) {
        ctx.setState(new ShippedState(code)); // PAID → SHIPPED
    }
    @Override
    public void confirm(OrderContext c)           { throw new InvalidOrderStateException("Already past confirm"); }
    @Override
    public void pay(OrderContext c, String t)     { throw new InvalidOrderStateException("Already paid"); }
    @Override
    public void deliver(OrderContext c)           { throw new InvalidOrderStateException("Not shipped yet"); }
    @Override
    public void cancel(OrderContext c, String r)  { ctx.setState(new RefundingState()); }
    public String getStatusName() { return "PAID"; }
}

// RESILIENCE4J CIRCUIT BREAKER = State Pattern
//
// Context: CircuitBreaker
// State Interface: CircuitBreakerState
// Concrete States: ClosedState, OpenState, HalfOpenState
//
// CLOSED: counts failures in sliding window.
//   if failureRate > threshold → setState(OPEN)
//   calls are allowed through
//
// OPEN: fails fast, no real calls.
//   starts a timer (waitDurationInOpenState)
//   timer expires → setState(HALF_OPEN)
//   returns CallNotPermittedException immediately
//
// HALF_OPEN: allows limited calls (permittedNumberOfCallsInHalfOpenState).
//   if success rate OK → setState(CLOSED)  ← recovered
//   if failure rate high → setState(OPEN)  ← still broken
//
// You call circuitBreaker.executeSupplier(() -> externalService.call())
// Circuit decides behavior based on its internal state — you never see the if-else
🔩 Where State Is Used in Java Frameworks
Resilience4j Circuit Breaker
CLOSED, OPEN, HALF_OPEN are concrete State classes. CircuitBreaker.executeSupplier() delegates to current state. State transitions triggered by failure rate thresholds and time windows — caller never sees the state machine.
Spring Statemachine
Dedicated State Machine framework for Spring. Define states, events, transitions, guards, and actions declaratively. Persists state machine state to DB for long-running workflows. Used for order processing, document approval flows.
Java Thread lifecycle
Thread.State: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED. Thread behavior changes based on state — start() only valid in NEW, join() waits in WAITING. JVM enforces valid state transitions.
TCP Connection States
CLOSED, LISTEN, SYN_SENT, SYN_RECEIVED, ESTABLISHED, FIN_WAIT, TIME_WAIT — textbook State pattern implementation in the kernel. Each state responds differently to the same TCP segment types.
HttpURLConnection
Internal states: before connect, connected, response received. Certain methods only valid in certain states — calling getOutputStream() after connect() throws if POST body not allowed in current state.
🎯 Interview Questions — State
State vs Strategy — same structure, different intent. Explain with examples.
Strategy: Client explicitly picks and injects the algorithm from OUTSIDE. Strategies don't know about each other. They're independent and interchangeable. Example: DiscountStrategy — PricingService chooses seasonal or loyalty; they're unrelated.
State: Object manages its own state internally. States know about each other and handle transitions. State changes are triggered by method calls or events — the client calls order.pay(txId) and the state machine decides the transition. Example: OrderContext — calling pay() on ConfirmedState creates PaidState; you don't decide which state comes next from outside.
Memory trick: Strategy = context RECEIVES the behavior (via constructor/setter). State = context CONTAINS and TRANSITIONS between behaviors internally based on events.
How do you persist state machine state in a Spring Boot application?
Option 1 — Store status string in entity: Persist statusName = "PAID" in the DB. On load: OrderContext.state = StateFactory.create(entity.getStatusName()). The @Transient state field is reconstructed from the persisted string. Simple and explicit.
Option 2 — Spring Statemachine + persistence: spring-statemachine-data-jpa provides JpaStateMachineRepository. State machine is serialized and stored as BLOB or JSON in DB. On load: stateMachineService.acquireStateMachine(machineId) restores full state including history.
Option 3 — Event sourcing: Don't store state at all — store events. Replay events to rebuild current state. DRAFT → OrderConfirmedEvent → PaidEvent → ShippedEvent = current state is SHIPPED. Ultimate audit trail.
How does Resilience4j Circuit Breaker implement State pattern internally?
CircuitBreaker is the Context. It holds a reference to current CircuitBreakerState. Concrete states: ClosedState, OpenState, HalfOpenState.
CLOSED: Calls are allowed. Records success/failure in a circular buffer (Count-based or Time-based sliding window). When failure rate exceeds threshold → transitionToOpenState().
OPEN: acquirePermission() immediately throws CallNotPermittedException (fast-fail, no real call). Starts a waitDurationInOpenState timer. On expiry → transitionToHalfOpenState().
HALF_OPEN: Allows exactly permittedNumberOfCallsInHalfOpenState calls. Evaluates results. If success rate OK → CLOSED. If failure rate high → OPEN again.
You call circuitBreaker.decorateSupplier(supplier).get() — the state machine is invisible to the caller.
How do you prevent invalid state transitions? What's the best approach?
Approach 1 — Throw in each state (shown above): Each state's method throws InvalidOrderStateException for invalid operations. Fine-grained, explicit error messages. Downside: lots of exception throwing methods to implement.
Approach 2 — Default method in interface: Add default void pay(...) { throw new InvalidOrderStateException(getStatusName() + " cannot be paid"); } to the interface. Only override in states where the transition is valid. Reduces boilerplate significantly — most states just inherit the default rejection.
Approach 3 — Spring Statemachine guards: Define guard conditions as Guard<State, Event> lambdas that return false when a transition is not allowed. The state machine silently ignores disallowed transitions or fires an error action — configurable behavior.
Best practice: Use Approach 2 (default interface methods) for simple state machines. Use Spring Statemachine or Axon Framework for complex workflows with many states and events.