Java Design Patterns & Resilience Cheat Sheet
A practical reference covering when design patterns earn their weight, thread-safe design patterns, patterns common in financial systems, Builder/records for clean construction, and resilience patterns for microservices.
Don’t Over-Engineer with Patterns
Over-engineering with patterns is as bad as ignoring them. Ask: does this pattern solve an actual problem I have, or am I adding complexity speculatively?
// BAD: Factory for a single concrete type that never changes
class PaymentFactory {
Payment create(String type) { return new CreditCardPayment(); }
// Only one implementation exists — factory adds zero value
}
// Just do: new CreditCardPayment()
// BAD: Strategy pattern with a single strategy
interface SortStrategy { void sort(List<?> list); }
class QuickSort implements SortStrategy { ... }
// If you only ever use QuickSort, the abstraction is noise
// GOOD: Strategy when you genuinely swap algorithms
interface FeeCalculator { BigDecimal calculate(Transaction tx); }
class StandardFee implements FeeCalculator { ... }
class PremiumFee implements FeeCalculator { ... }
class WaivedFee implements FeeCalculator { ... }
// Real variation → pattern earns its weight
Thread-Safe Design Patterns
Patterns specifically for thread-safe code:
// Immutable object — thread-safe with zero synchronization
final class Money {
private final long amount;
private final String currency;
public Money(long amount, String currency) { /* assign */ }
public Money add(Money other) {
return new Money(this.amount + other.amount, currency); // new instance
}
// no setters, all fields final
}
// ThreadLocal — per-thread state without synchronization
static final ThreadLocal<SimpleDateFormat> formatter =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
// Each thread gets its own SimpleDateFormat — no shared state
// Producer-Consumer with BlockingQueue
BlockingQueue<Transaction> queue = new ArrayBlockingQueue<>(1000);
// Producer thread
new Thread(() -> {
while (running) queue.put(nextTransaction());
}).start();
// Consumer thread
new Thread(() -> {
while (running) process(queue.take());
}).start();
// Double-Checked Locking — ONLY correct with volatile
class Config {
private static volatile Config instance; // volatile is REQUIRED
static Config getInstance() {
if (instance == null) {
synchronized (Config.class) {
if (instance == null) instance = new Config(); // check again
}
}
return instance;
}
}
Patterns Common in Financial Systems
Patterns that come up repeatedly in financial systems:
// Command — auditable, replayable transactions
interface Command {
void execute();
void undo();
CommandAudit toAuditRecord();
}
class TransferCommand implements Command {
private final Account from, to;
private final Money amount;
public void execute() { from.debit(amount); to.credit(amount); }
public void undo() { to.debit(amount); from.credit(amount); }
// Every action is an object — store, replay, undo
}
// Decorator — wrap service with cross-cutting concerns
interface PaymentService { Receipt pay(Payment p); }
class LoggingPaymentService implements PaymentService {
private final PaymentService delegate;
public Receipt pay(Payment p) {
log.info("paying {}", p);
Receipt r = delegate.pay(p);
log.info("paid {}", r);
return r;
}
}
class AuthPaymentService implements PaymentService {
public Receipt pay(Payment p) {
checkAuthorization(p); // wraps delegate
return delegate.pay(p);
}
}
// Stack them: auth → logging → actual service
// Saga — distributed transaction across microservices
// Each step has a compensating action
// Step 1: Reserve inventory → Compensate: release inventory
// Step 2: Charge payment → Compensate: refund payment
// Step 3: Update ledger → Compensate: reverse ledger entry
// If step 3 fails, run compensations for 2 and 1 in reverse order
Builder & Records
Builder prevents telescoping constructors and enforces invariants. Java 16+ records give you immutable DTOs for free.
// Builder — enforces invariants, readable construction
class TransferRequest {
private final String fromAccount;
private final String toAccount;
private final Money amount;
private final String reference; // optional
private TransferRequest(Builder b) {
this.fromAccount = Objects.requireNonNull(b.fromAccount);
this.toAccount = Objects.requireNonNull(b.toAccount);
this.amount = b.amount;
this.reference = b.reference;
}
static class Builder {
String fromAccount, toAccount, reference;
Money amount;
Builder from(String acc) { this.fromAccount = acc; return this; }
Builder to(String acc) { this.toAccount = acc; return this; }
Builder amount(Money m) { this.amount = m; return this; }
Builder ref(String r) { this.reference = r; return this; }
TransferRequest build() { return new TransferRequest(this); }
}
}
// Usage:
var req = new TransferRequest.Builder()
.from("NL91ABNA0417164300")
.to("NL69INGB0123456789")
.amount(Money.of(1000, "EUR"))
.build();
// Java 16+ record — immutable DTO for free
record PaymentDTO(String id, long amount, String currency, Instant at) {}
// Gives you: constructor, getters, equals, hashCode, toString — all final
Resilience Patterns (Resilience4j)
Essential for microservice resilience at scale. From Resilience4j:
Request
│
▼
┌──────────────┐ OPEN (tripped)
│Circuit Breaker│──────────────────▶ fail fast (no call made)
│ CLOSED │
│ (passing) │◀─────────────────── HALF-OPEN (probe)
└──────┬───────┘
│ failure threshold exceeded
└──────────────────────────▶ OPEN
// Circuit Breaker (Resilience4j)
CircuitBreaker cb = CircuitBreaker.ofDefaults("paymentService");
Supplier<Receipt> decorated = CircuitBreaker
.decorateSupplier(cb, () -> paymentService.pay(payment));
Try.ofSupplier(decorated)
.recover(CallNotPermittedException.class, e -> fallback());
// Retry with exponential backoff
Retry retry = Retry.of("payment", RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(100))
.intervalFunction(IntervalFunction.ofExponentialBackoff())
.retryOnException(e -> e instanceof TransientException)
.build());
// Bulkhead — limit concurrent calls to a service
Bulkhead bulkhead = Bulkhead.of("db", BulkheadConfig.custom()
.maxConcurrentCalls(20)
.maxWaitDuration(Duration.ofMillis(500))
.build());
// Timeout
TimeLimiter timeLimiter = TimeLimiter.of(Duration.ofSeconds(2));
// Compose them all:
// TimeLimiter → CircuitBreaker → Retry → Bulkhead → actual call
