Skip to content
TobussSystems in Practice

Java Design Patterns & Resilience Cheat Sheet

4 min read

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