Definition
- 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.
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:
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.
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); }
}
}
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.
package com.bank.event;
public record TransferCompletedEvent(String transferId, String fromAccount, String toAccount,
java.math.BigDecimal amount) {}
// 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.
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
}
}
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.
// 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 |
// 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.
// 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
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.
---