Refactoring Patterns
๐ก Overview
This document summarizes the analysis and implementation of various design patterns for refactoring the account service in the fintech application. The goal was to reduce branching complexity and improve code organization in the Deposit
and Withdraw
methods.
๐ฆ Reference: pkg/registry
The pkg/registry package provides a flexible, extensible registry system for managing entities (users, accounts, currencies, etc.) with support for:
- Abstractions:
Entity
interface (property-style getters:ID()
,Name()
,Active()
,Metadata()
, etc.)RegistryProvider
interface for CRUD, search, metadata, and lifecycle operations-
Observer/event bus, validation, caching, persistence, metrics, and health interfaces
-
Patterns & Architecture:
- Clean separation of interface and implementation layers
- Builder and factory patterns for registry construction
- Event-driven and observer patterns for entity lifecycle events
-
Caching and persistence strategies (in-memory, file-based, etc.)
-
Usage Examples:
- Register, retrieve, update, and unregister entities
- Use memory cache, file persistence, and custom validation
-
Event-driven hooks for entity changes
-
Best Practices:
- Use property-style getters for all entities (e.g.,
Name()
, notGetName()
) - Prefer registry interfaces for dependency inversion and testability
- Leverage event bus and observer for decoupled side effects (metrics, logging, etc.)
- Use the builder for complex configuration (caching, validation, persistence)
๐งช Example: Registering an Entity
user := registry.NewBaseEntity("user-1", "John Doe")
user.Metadata()["email"] = "john@example.com"
err := registry.Register(ctx, user)
๐งช Example: Custom Registry with Caching & HandleProcessed
reg, err := registry.NewRegistryBuilder().
WithName("prod-reg").
WithCache(1000, 10*time.Minute).
WithPersistence("/data/entities.json", 30*time.Second).
BuildRegistry()
Why use the registry?
The registry pattern centralizes entity management, supports extensibility (events, validation, caching), and enforces clean architecture boundaries.
See also:
- pkg/registry/README.md
for full documentation
- pkg/registry/interface.go
for all abstractions
- pkg/registry/examples_test.go
for usage patterns
โ ๏ธ Initial Problem
- Significant code duplication (~150 lines of nearly identical logic)
- Complex branching around currency conversion and transaction handling
- Mixed responsibilities (validation, conversion, persistence, logging)
- Poor maintainability due to tightly coupled logic
๐ ๏ธ Strategy Pattern
Approach:
- Extract common operation logic into a shared method using the strategy pattern for operation type.
- Use an
operationHandler
interface and concrete strategies for deposit/withdraw.
Key Code:
// types.go
type OperationType string
const (
OperationDeposit OperationType = "deposit"
OperationWithdraw OperationType = "withdraw"
)
type operationHandler interface {
execute(account *account.Account, userID uuid.UUID, money mon.Money) (*account.Transaction, error)
}
// handlers.go
type depositHandler struct{}
func (h *depositHandler) execute(account *account.Account, userID uuid.UUID, money mon.Money) (*account.Transaction, error) {
return account.Deposit(userID, money)
}
When to Use:
- You have similar operations (deposit/withdraw) with shared logic but different details.
Benefits:
Pattern | Branching | Extensibility | Testability | Complexity | Go Idiomatic |
---|---|---|---|---|---|
Strategy | Low | Good | Good | Medium | โ |
Command | None | Excellent | Excellent | High | โ ๏ธ |
Chain of Responsibility | None | Excellent | Excellent | Medium | โ |
Event-Driven | None | Excellent | Good | High | โ ๏ธ |
๐งฐ Implementation Status
- โ Strategy Pattern: Implemented and fully discarded
- โ Chain of Responsibility: Implemented
- ๐ Command Pattern: Analyzed, ready for implementation if needed
- โ Event-Driven: Implemented
๐งช Code Quality Metrics
Before Refactoring
- Lines of Code: ~566 lines in single file
- Cyclomatic Complexity: High (multiple nested if-else blocks)
- Code Duplication: ~150 lines duplicated between Deposit/Withdraw
- Maintainability: Poor (tightly coupled logic)
After Strategy Pattern
- Lines of Code: ~700 lines across 7 focused files
- Cyclomatic Complexity: Reduced (linear flow in executeOperation)
- Code Duplication: Eliminated
- Maintainability: Excellent (clear separation of concerns)
Expected After Chain of Responsibility
- Lines of Code: ~800 lines across 10+ focused files
- Cyclomatic Complexity: Minimal (linear handler chain)
- Code Duplication: None
- Maintainability: Outstanding (single responsibility per handler)
๐ Recommendations
For Current Use Case
Chain of Responsibility is the best fit because:
- Eliminates all branching in the service layer
- Maintains Go idioms and simplicity
- Provides excellent extensibility
- Each handler has a single, clear responsibility
- Easy to test and maintain
For Future Extensions
Consider hybrid approaches:
- Strategy + Chain of Responsibility: Use strategy for operation type, chain for execution steps
- Synchronous + Event-Driven: Keep core business logic synchronous, use events for side effects (audit, notifications)
๐ฎ Conclusion
The refactoring journey demonstrates how different design patterns can address the same problem with varying trade-offs. The Strategy Pattern provided immediate benefits, while Chain of Responsibility offers the best long-term solution for this specific use case.
The key insight is that pattern selection should be driven by specific requirements rather than following a one-size-fits-all approach. For fintech applications requiring high reliability and maintainability, the Chain of Responsibility pattern provides the optimal balance of simplicity, extensibility, and Go idiomaticity.