SOLID Architecture Guidelines
Apply these principles when writing or modifying code. Use them as tie-breakers when design decisions conflict.
Core Goals (Priority Order)
- •Maintainability - Easy to change without breaking unrelated parts
- •Testability - Core logic testable without I/O or UI
- •Determinism - Reproducible given same inputs/seeded RNG
- •Separation of Concerns - Domain, infrastructure, UI clearly separated
Composition Over Inheritance
Favor small, focused types composed together rather than deep inheritance trees. When tempted to extend a class, first ask: "Can this be achieved through composition instead?"
SOLID Principles
Single Responsibility (SRP)
Each module/type/function has one reason to change.
Violation signals:
- •Cannot describe purpose in one sentence
- •Domain logic mixed with infrastructure
- •Multiple unrelated reasons to modify the file
Action: Split into focused modules with clear, singular purposes.
Open/Closed (OCP)
Extend via new implementations, not constant modification.
Violation signals:
- •Adding behavior requires modifying existing code
- •Growing switch/if-else chains for new cases
- •Frequent changes to stable modules
Action: Add behavior through new modules and composition, not conditionals.
Liskov Substitution (LSP)
Subtypes must work anywhere their base type is expected.
Violation signals:
- •Subtypes that throw on inherited operations
- •Subtypes that ignore/no-op inherited methods
- •Deep inheritance hierarchies
Action: Prefer interfaces over deep hierarchies. Ensure substitutability.
Interface Segregation (ISP)
Depend only on the minimal surface needed.
Violation signals:
- •Large interfaces with many methods
- •Consumers only using subset of interface
- •"Fat" interfaces forcing empty implementations
Action: Create small, specific interfaces. Split large ones.
Dependency Inversion (DIP)
Depend on abstractions, not concretions.
Violation signals:
- •Direct imports of concrete implementations
- •Global singletons for RNG, config, I/O
- •Hard-coded dependencies
Action: Inject dependencies. Use explicit context/environment objects passed to systems.
Module Organization
File Granularity
- •Non-trivial types (classes, structs, complex components): Dedicate a file
- •Related utilities/functions: Group by cohesive purpose in single module
- •Avoid: Grab-bag "utils" files - group by purpose instead
Layering
Establish clear dependency directions:
core → domain → application → UI
Rules:
- •Lower layers MUST NOT import from higher layers
- •Mark any temporary violations and track cleanup
- •Use barrel/index files only for public APIs
- •Internal modules import directly; external consumers use public API
- •Avoid circular dependencies
Side Effects & Boundaries
Pure vs Impure Separation
Separate pure computation from side effects.
Pure (no side effects):
- •Calculations, transformations, business logic
- •Receives all inputs as parameters
- •Returns results without modifying external state
Impure (side effects):
- •I/O operations (file, network, database)
- •Random number generation
- •Time/date operations
- •Logging, metrics
Dependency Injection
Systems receive these via injection for deterministic testing:
- •Clocks
- •RNG (seeded for reproducibility)
- •I/O adapters
Boundary Modules
Push I/O and external integrations to small, well-named boundary modules at system edges.
When refactoring: Bias toward making core logic purer and pushing side effects outward.
Quick Reference Checklist
Before committing code changes:
- • Can each module's purpose be described in one sentence?
- • Is domain logic free from infrastructure concerns?
- • Are dependencies injected, not hard-coded?
- • Do lower layers avoid importing from higher layers?
- • Are side effects pushed to system boundaries?
- • Is the code testable without mocking I/O?