# V2 Modularity Requirements ## Core Principle: Standalone Components **Every component MUST be able to run independently OR as part of the integrated system.** ## Component Independence Rules ### 1. Zero Hard Dependencies - Each component communicates through well-defined interfaces only - NO direct imports between sibling components - NO shared state except through explicit interfaces - Each component has its own configuration ### 2. Standalone Executability Every component must support: ```go // Example: Each parser can be used standalone parser := uniswap_v2.NewParser(logger, cache) event, err := parser.ParseLog(log, tx) // OR as part of factory factory := parsers.NewFactory() factory.Register(ProtocolUniswapV2, parser) ``` ### 3. Interface-First Design Define interfaces BEFORE implementation: ```go // pkg/parsers/interface.go type Parser interface { ParseLog(log *types.Log, tx *types.Transaction) (*Event, error) ParseReceipt(receipt *types.Receipt, tx *types.Transaction) ([]*Event, error) SupportedProtocols() []Protocol ValidateEvent(event *Event) error } // Each parser implements this independently ``` ## Component Modularity Matrix | Component | Standalone Use Case | Integrated Use Case | Interface | |-----------|-------------------|---------------------|-----------| | UniswapV2Parser | Parse individual V2 transactions | Part of ParserFactory | `Parser` | | UniswapV3Parser | Parse individual V3 transactions | Part of ParserFactory | `Parser` | | PoolCache | Standalone pool lookup service | Shared cache for all parsers | `PoolCache` | | EventValidator | Validate any event independently | Part of validation pipeline | `Validator` | | AddressIndex | Standalone address lookup | Part of multi-index cache | `Index` | | TokenPairIndex | Standalone pair lookup | Part of multi-index cache | `Index` | | BackgroundValidator | Standalone validation service | Part of monitoring pipeline | `BackgroundValidator` | ## Directory Structure for Modularity ``` pkg/ ├── parsers/ │ ├── interface.go # Parser interface (shared) │ ├── factory.go # Factory for integration │ ├── uniswap_v2/ # Standalone package │ │ ├── parser.go # Can be imported independently │ │ ├── parser_test.go # Self-contained tests │ │ └── README.md # Standalone usage docs │ ├── uniswap_v3/ # Standalone package │ │ ├── parser.go │ │ ├── parser_test.go │ │ └── README.md │ └── sushiswap/ # Standalone package │ ├── parser.go │ ├── parser_test.go │ └── README.md │ ├── cache/ │ ├── interface.go # Cache interfaces │ ├── pool_cache.go # Main cache (uses indexes) │ ├── indexes/ # Standalone index packages │ │ ├── address/ │ │ │ ├── index.go # Standalone address index │ │ │ └── index_test.go │ │ ├── tokenpair/ │ │ │ ├── index.go # Standalone pair index │ │ │ └── index_test.go │ │ └── liquidity/ │ │ ├── index.go # Standalone liquidity index │ │ └── index_test.go │ ├── validation/ │ ├── interface.go # Validator interface │ ├── validator.go # Main validator │ ├── rules/ # Standalone rule packages │ │ ├── zero_address/ │ │ │ ├── rule.go # Standalone rule │ │ │ └── rule_test.go │ │ ├── zero_amount/ │ │ │ ├── rule.go │ │ │ └── rule_test.go │ │ └── pool_cache/ │ │ ├── rule.go │ │ └── rule_test.go │ └── background/ │ ├── validator.go # Standalone background validator │ └── validator_test.go ``` ## Testing Requirements for Modularity ### 1. Unit Tests (Component Isolation) Each component has 100% independent unit tests: ```go // pkg/parsers/uniswap_v2/parser_test.go func TestParser_Standalone(t *testing.T) { // NO dependencies on other parsers // NO dependencies on factory // Uses mocks for interfaces only logger := NewMockLogger() cache := NewMockCache() parser := NewParser(logger, cache) event, err := parser.ParseLog(mockLog, mockTx) assert.NoError(t, err) assert.NotNil(t, event) } ``` ### 2. Integration Tests (Component Composition) Test components working together: ```go // tests/integration/parser_factory_test.go func TestParserFactory_Integration(t *testing.T) { // Test all parsers working through factory factory := parsers.NewFactory() // Each parser registered independently factory.Register(ProtocolUniswapV2, uniswap_v2.NewParser(logger, cache)) factory.Register(ProtocolUniswapV3, uniswap_v3.NewParser(logger, cache)) // Test factory routing parser, err := factory.GetParser(ProtocolUniswapV2) assert.NoError(t, err) } ``` ### 3. Standalone Executability Tests Each component has example main: ```go // examples/uniswap_v2_parser/main.go func main() { // Demonstrate standalone usage logger := logger.New("info", "text", "") cache := cache.NewPoolCache() parser := uniswap_v2.NewParser(logger, cache) // Parse single transaction event, err := parser.ParseLog(log, tx) if err != nil { log.Fatal(err) } fmt.Printf("Parsed event: %+v\n", event) } ``` ## Dependency Injection Pattern All components use constructor injection: ```go // Bad: Hard dependency type UniswapV2Parser struct { cache *PoolCache // Hard dependency! } // Good: Interface dependency type UniswapV2Parser struct { cache PoolCache // Interface - can be mocked or replaced } func NewParser(logger Logger, cache PoolCache) *UniswapV2Parser { return &UniswapV2Parser{ logger: logger, cache: cache, } } ``` ## Configuration Independence Each component has its own config: ```go // pkg/parsers/uniswap_v2/config.go type Config struct { EnableValidation bool CacheTimeout time.Duration MaxRetries int } // Standalone usage config := uniswap_v2.Config{ EnableValidation: true, CacheTimeout: 5 * time.Minute, MaxRetries: 3, } parser := uniswap_v2.NewParserWithConfig(logger, cache, config) // Integrated usage factory.RegisterWithConfig( ProtocolUniswapV2, parser, config, ) ``` ## Interface Contracts ### Minimal Interface Surface Each interface has 1-3 methods maximum: ```go // Good: Focused interface type Parser interface { ParseLog(log *types.Log, tx *types.Transaction) (*Event, error) } // Bad: God interface type Parser interface { ParseLog(log *types.Log, tx *types.Transaction) (*Event, error) ParseReceipt(receipt *types.Receipt, tx *types.Transaction) ([]*Event, error) ValidateEvent(event *Event) error GetStats() Stats Configure(config Config) error // ... too many responsibilities } ``` ### Interface Segregation Split large interfaces: ```go // Split responsibilities type LogParser interface { ParseLog(log *types.Log, tx *types.Transaction) (*Event, error) } type ReceiptParser interface { ParseReceipt(receipt *types.Receipt, tx *types.Transaction) ([]*Event, error) } type EventValidator interface { ValidateEvent(event *Event) error } // Compose as needed type Parser interface { LogParser ReceiptParser EventValidator } ``` ## Build Tags for Optional Components Use build tags for optional features: ```go // pkg/parsers/uniswap_v2/parser.go // +build !minimal // Full implementation with all features // pkg/parsers/uniswap_v2/parser_minimal.go // +build minimal // Minimal implementation for embedded systems ``` Build options: ```bash # Full build (all components) go build ./... # Minimal build (core only) go build -tags minimal ./... # Custom build (specific parsers only) go build -tags "uniswap_v2 uniswap_v3" ./... ``` ## Component Communication Patterns ### 1. Synchronous (Direct Call) For tight coupling when needed: ```go event, err := parser.ParseLog(log, tx) ``` ### 2. Asynchronous (Channels) For loose coupling: ```go eventChan := make(chan *Event, 100) go parser.ParseAsync(logs, eventChan) ``` ### 3. Pub/Sub (Event Bus) For many-to-many communication: ```go bus := eventbus.New() parser.Subscribe(bus, "parsed") validator.Subscribe(bus, "parsed") bus.Publish("parsed", event) ``` ### 4. Interface Composition For static composition: ```go type CompositeParser struct { v2 *uniswap_v2.Parser v3 *uniswap_v3.Parser } func (c *CompositeParser) Parse(log *types.Log) (*Event, error) { // Route to appropriate parser } ``` ## Success Criteria for Modularity Each component MUST: - [ ] Compile independently (`go build ./pkg/parsers/uniswap_v2`) - [ ] Test independently (`go test ./pkg/parsers/uniswap_v2`) - [ ] Run standalone example (`go run examples/uniswap_v2_parser/main.go`) - [ ] Have zero sibling dependencies - [ ] Communicate only through interfaces - [ ] Include standalone usage documentation - [ ] Pass integration tests when composed - [ ] Support mock implementations for testing ## Anti-Patterns to Avoid ### ❌ Circular Dependencies ```go // pkg/parsers/uniswap_v2/parser.go import "pkg/parsers/uniswap_v3" // BAD! // pkg/parsers/uniswap_v3/parser.go import "pkg/parsers/uniswap_v2" // BAD! ``` ### ❌ Shared Mutable State ```go // BAD: Global shared state var globalCache *PoolCache func (p *Parser) Parse(log *types.Log) (*Event, error) { pool := globalCache.Get(log.Address) // BAD! } ``` ### ❌ Hard-coded Dependencies ```go // BAD: Creates own dependencies func NewParser() *Parser { return &Parser{ cache: NewPoolCache(), // BAD! Should be injected } } ``` ### ❌ Leaky Abstractions ```go // BAD: Exposes internal structure type Parser interface { GetInternalCache() *PoolCache // BAD! Leaks implementation } ``` ## Migration Strategy When moving from V1 to V2: 1. **Extract Interface**: Define interface from V1 implementation 2. **Create Package**: Move to standalone package 3. **Inject Dependencies**: Replace hard dependencies with interfaces 4. **Add Tests**: Unit tests for standalone operation 5. **Create Example**: Standalone usage example 6. **Document**: README with standalone and integrated usage 7. **Verify**: Check all modularity criteria ## Component Checklist Before marking any component as "complete": - [ ] Compiles independently - [ ] Tests independently (>90% coverage) - [ ] Has standalone example - [ ] Has README with usage - [ ] Zero sibling dependencies - [ ] All dependencies injected through interfaces - [ ] Can be mocked for testing - [ ] Integrated into factory/orchestrator - [ ] Integration tests pass - [ ] Performance benchmarks exist - [ ] Documentation complete --- **Principle**: If you can't run it standalone, it's not modular enough. **Guideline**: If you can't mock it, it's too coupled. **Rule**: If it has circular dependencies, redesign it.