Back to API Design Best Practices
Q18Question 18 of 22

Design Patterns: SOLID, Strategy, Builder, Observer, Circuit Breaker, DTO

Key Concepts
  • Strategy pattern swaps fee calculation algorithms behind a common interface, stored in a Map and selected at runtime.
  • Builder pattern constructs complex objects step by step, avoiding constructors with many positional arguments.
  • Observer pattern decouples an event from its reactions: one transfer event triggers audit, SMS, and dashboard independently.
  • Circuit Breaker stops calling a failing dependency after a threshold, failing fast and returning a fallback.
  • DTO separates the API contract from the JPA entity, preventing field leaks and schema coupling.
  • SOLID is the five principles (SRP, OCP, LSP, ISP, DIP) that make Strategy, Observer, and DTO clean and testable.

Definition

Strategy swaps behaviour behind an interface, Builder assembles complex objects readably, Observer fans out events to listeners, Circuit Breaker fails fast on a sick dependency, DTO shields the API from the entity, and SOLID holds it all together.
  • Strategy – defines a family of interchangeable algorithms behind a common interface so behaviour can be selected at runtime.
  • Builder – constructs a complex object step by step instead of through a large constructor, producing readable and immutable results.
  • Observer – lets one event notify many independent listeners without the publisher knowing who they are.
  • Circuit Breaker – stops calling a failing dependency after a threshold of errors, immediately returning a fallback and allowing recovery.
  • DTO – carries data across the API boundary, decoupling the public contract from the JPA entity.
  • SOLID – five design principles (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) that keep code maintainable and testable.

At National Bank of India: Strategy is having different fee clerks for regular, premium, and promotional customers. Builder is filling a loan form field by field instead of shouting fifteen details at once. Observer is ringing one bell when a transfer completes and having audit, SMS, and dashboard desks react on their own. Circuit Breaker is putting up a "counter closed" sign when the central server keeps timing out. DTO is the printed receipt you hand to Rajesh, not the bank's full internal ledger row. SOLID is the operating manual keeping every desk doing one job cleanly.

Understanding the Concepts

What is the Strategy pattern?

Strategy replaces a long if-else chain with a set of small classes that all implement one interface. In a banking service, fee calculation differs by customer tier.

JAVA
package com.bank.strategy;

import java.math.BigDecimal;

public interface FeeStrategy {
    BigDecimal calculateFee(BigDecimal amount);
}

public class RegularFeeStrategy implements FeeStrategy {
    public BigDecimal calculateFee(BigDecimal amount) {
        return amount.multiply(new BigDecimal("0.005")); // 0.5%
    }
}

public class PremiumFeeStrategy implements FeeStrategy {
    public BigDecimal calculateFee(BigDecimal amount) {
        return amount.multiply(new BigDecimal("0.002")); // 0.2%
    }
}

public class ZeroFeeStrategy implements FeeStrategy {
    public BigDecimal calculateFee(BigDecimal amount) {
        return BigDecimal.ZERO; // festival promotion
    }
}

Strategies are stored in a Map and selected at runtime:

JAVA
Map<String, FeeStrategy> strategies = Map.of(
        "regular", new RegularFeeStrategy(),
        "premium", new PremiumFeeStrategy(),
        "zero",    new ZeroFeeStrategy());

FeeStrategy strategy = strategies.get(customerTier);
BigDecimal fee = strategy.calculateFee(new BigDecimal("50000"));

Adding a new tier means writing one class and one Map entry, with zero changes to existing code.

What is the Builder pattern?

Builder constructs an object with many fields step by step, avoiding a constructor with positional arguments where from and to accounts can be swapped.

JAVA
package com.bank.model;

import java.math.BigDecimal;

public class TransferResult {
    private final String transferId;
    private final String fromAccount;
    private final String toAccount;
    private final BigDecimal amount;
    private final BigDecimal fee;
    private final String status;

    private TransferResult(Builder b) {
        this.transferId = b.transferId;
        this.fromAccount = b.fromAccount;
        this.toAccount = b.toAccount;
        this.amount = b.amount;
        this.fee = b.fee;
        this.status = b.status;
    }

    public static Builder builder() { return new Builder(); }

    public static class Builder {
        private String transferId, fromAccount, toAccount, status;
        private BigDecimal amount, fee;
        public Builder transferId(String v) { transferId = v; return this; }
        public Builder fromAccount(String v) { fromAccount = v; return this; }
        public Builder toAccount(String v) { toAccount = v; return this; }
        public Builder amount(BigDecimal v) { amount = v; return this; }
        public Builder fee(BigDecimal v) { fee = v; return this; }
        public Builder status(String v) { status = v; return this; }
        public TransferResult build() { return new TransferResult(this); }
    }
}
JAVA
TransferResult result = TransferResult.builder()
        .transferId("TXF-00001")
        .fromAccount("SAV2001")
        .toAccount("CUR3001")
        .amount(new BigDecimal("50000"))
        .fee(new BigDecimal("250"))
        .status("COMPLETED")
        .build();

What is the Observer pattern?

Observer decouples an event from the things that react to it. When a transfer completes, several actions must happen: audit, SMS, dashboard update. The transfer code publishes one event and listeners react independently.

JAVA
package com.bank.event;

public record TransferCompletedEvent(String transferId, String fromAccount, String toAccount,
                                      java.math.BigDecimal amount) {}
JAVA
// Publisher:
applicationEventPublisher.publishEvent(
        new TransferCompletedEvent("TXF-00001", "SAV2001", "CUR3001", amount));

// Listeners – each independent, never touching the transfer code:
@EventListener
public void onTransfer(TransferCompletedEvent e) {
    auditService.record(e.transferId(), "TRANSFER", "SUCCESS");
}

@EventListener
public void onTransfer(TransferCompletedEvent e) {
    smsService.send(e.fromAccount(), "Transfer " + e.transferId() + " completed.");
}

Adding a new reaction is one listener class and never destabilizes the transfer code.

What is the Circuit Breaker pattern?

Circuit Breaker protects your service from a dependency that has become slow or unavailable. After a threshold of failures it "opens" and returns a fallback immediately.

JAVA
package com.bank.service;

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import java.math.BigDecimal;

@Service
public class FxRateService {

    @CircuitBreaker(name = "fxRate", fallbackMethod = "cachedRate")
    public BigDecimal liveRate(String pair) {
        return externalFxApi.getRate(pair); // may timeout
    }

    private BigDecimal cachedRate(String pair, Throwable t) {
        return lastKnownRate.get(pair); // fallback to cached value
    }
}
DIAGRAM
Closed → calls go through normally
Open   → calls skip the dependency, return fallback immediately
Half-Open → one trial call to test if the dependency recovered

What is the DTO pattern?

DTO separates the data exposed at the API boundary from the entity stored in the database. The entity has internal columns that customers must never see.

JAVA
// Entity – internal:
@Entity
public class Account {
    @Id private String id;
    private String holderName;
    private String passwordHash;       // never expose
    private int internalRiskScore;     // never expose
    private BigDecimal balance;
}

// DTO – public contract:
public record AccountResponse(String accountId, String holderName, BigDecimal balance) {}

// Mapping in the service:
public AccountResponse toResponse(Account a) {
    return new AccountResponse(a.getId(), a.getHolderName(), a.getBalance());
}

How do SOLID principles tie everything together?

| Principle | How it applies |
|---|---|
| Single Responsibility | TransferService moves money, AuditService records, FeeStrategy computes fees |
| Open/Closed | New fee strategy = new class, zero edits to existing code |
| Liskov Substitution | Any FeeStrategy implementation works where the interface is expected |
| Interface Segregation | FeeStrategy has one method, not a fat interface |
| Dependency Inversion | TransferService depends on the FeeStrategy interface, not a concrete class |

JAVA
// Dependency Inversion in action – inject via constructor:
public class TransferService {
    private final Map<String, FeeStrategy> feeStrategies;

    public TransferService(Map<String, FeeStrategy> feeStrategies) {
        this.feeStrategies = feeStrategies; // depends on interface, not concrete
    }

    // In a test: new TransferService(Map.of("regular", new RegularFeeStrategy()))
}

What are common mistakes?

The most common mistake is a god class that handles transfers, fee calculation, SMS, and audit all in one method. The second mistake is a giant if-else chain for fee types instead of the Strategy pattern. The third is returning entities instead of DTOs.

JAVA
// Wrong – god method:
public void transfer(String from, String to, BigDecimal amount) {
    // calculate fee with if-else
    // move money
    // send SMS
    // write audit
    // update dashboard
    // 500 lines in one method
}

// Correct – separated responsibilities:
transferService.execute(request);      // Strategy for fee, Repository for persistence
// Observer pattern handles SMS, audit, dashboard via event listeners

What should a senior engineer know?

These patterns reinforce each other. Strategy plus a Map registry implements Open/Closed and Dependency Inversion simultaneously. Observer keeps the transfer code stable as new reactions are added. Circuit Breaker prevents one sick dependency from freezing the whole service. DTO protects the API boundary. And SOLID is the foundation that makes all of them clean, testable, and maintainable. In a banking interview, showing how these patterns solve specific pains in real code is what distinguishes a senior answer from a textbook recitation.

Visual Overview

DIAGRAM
  How Patterns Fit Together in a Transfer:

  Client ──TransferRequest (DTO)──→ Controller
                                        │
                                        ▼
                                   TransferService
                                   ├── FeeStrategy (Strategy pattern)
                                   │   └── strategies.get(tier).calculateFee()
                                   ├── FxRateService (Circuit Breaker)
                                   │   └── @CircuitBreaker → fallback on failure
                                   ├── accountRepo.save() (Entity)
                                   └── publishEvent(TransferCompletedEvent)
                                                    │
                                        ┌───────────┼───────────┐
                                        │           │           │
                                   AuditListener  SMSListener  DashboardListener
                                   (Observer)     (Observer)   (Observer)

  Response: TransferResult (Builder) → mapped to DTO → Client

Key Takeaway

In a banking transfer service, a small set of design patterns solves specific, real problems. The Strategy pattern handles fee calculation by defining a FeeStrategy interface with one implementation per customer tier, stored in a Map and selected at runtime, so adding a new fee type means writing one class with zero edits to existing logic. The Builder pattern constructs the transfer result object with named fields, eliminating the bug class where from and to accounts were swapped in a positional constructor.

The Observer pattern keeps the core money-movement code clean. When a transfer completes, one event is published and independent listeners for audit, SMS, and dashboard react on their own. Adding a new reaction never destabilizes the transfer code. The Circuit Breaker protects against a flaky external rate provider: after a few failures it opens, stops hammering the sick service, and returns the last cached rate.

The DTO pattern protects the API boundary. The entity has an internal version column and a risk score that customers must never see, so the controller returns a trimmed response record. Underneath all of this sits SOLID: each class has one responsibility, the service depends on interfaces rather than concrete classes so mocks work in tests, and the Strategy registry gives Open/Closed and Dependency Inversion simultaneously.

Remember one sentence: Strategy swaps behaviour, Builder assembles objects, Observer decouples events, Circuit Breaker fails fast, DTO protects the boundary, and SOLID holds them all together.

---

Share
Use ← → arrow keys to navigate