ADR 0003: Adopt Layered Architecture¶
Status¶
Accepted
Date¶
2025-11-21
Context¶
Shtym needs to implement subprocess execution and output handling for pass-through mode (Issue #3), with future expansion to LLM integration. The current flat module structure (cli.py, core.py) lacks clear separation of concerns, which will make future LLM integration and testing more difficult.
A clear architectural pattern is needed to:
- Separate I/O concerns from business logic
- Enable easy testing without actual I/O operations
- Provide clear boundaries for future LLM integration
- Maintain code organization as functionality grows
Decision¶
Adopt a layered architecture with four distinct layers:
src/shtym/
├── _version.py # Version constant
├── __init__.py # Package exports
├── cli.py # Presentation Layer
├── application.py # Application Layer
├── domain.py # Domain Layer
└── infrastructure/ # Infrastructure Layer
├── __init__.py
└── stdio.py # stdout writing
Layer responsibilities:
- Presentation Layer (cli.py): CLI argument parsing, error handling, user-facing messages
- Application Layer (application.py): Orchestration of domain and infrastructure components
- Domain Layer (domain.py): Core text processing logic (pass-through, future LLM summarization)
- Infrastructure Layer (infrastructure/): External I/O operations (stdio, future LLM API clients)
Rationale¶
- Testability: Each layer can be tested independently. Infrastructure I/O can be mocked in unit tests
- Separation of concerns: Clear boundaries between user interface, orchestration, business logic, and external dependencies
- Future LLM integration: LLM clients will naturally fit in infrastructure layer alongside stdio
- Maintainability: Changes to I/O mechanisms or CLI don't affect business logic
- Standard pattern: Layered architecture is well-understood and documented
- Lightweight: Only four layers, avoiding over-engineering for small project
Implications¶
Positive Implications¶
- Unit tests can focus on pure logic without I/O
- Infrastructure components are swappable (e.g., different LLM providers)
- Clear mental model for future contributors
- Dependency flow is explicit (presentation → application → domain ← infrastructure)
- Easy to add new infrastructure adapters (file I/O, network, etc.)
Concerns¶
- Slightly more files than flat structure (mitigation: only 3-4 additional modules, manageable)
- Requires discipline to maintain layer boundaries (mitigation: documented in ADR, enforced in code review)
- Possible over-engineering for simple pass-through (mitigation: pattern pays off immediately when LLM integration begins)
Alternatives¶
Flat Module Structure¶
Keep current flat structure with cli.py and core.py.
- Pros: Fewer files, simpler initial setup
- Cons: Tight coupling between I/O and logic, hard to test, unclear where to add LLM clients
- Reason for rejection: Technical debt accumulates quickly; refactoring later is harder than starting with clear structure
Hexagonal Architecture (Ports and Adapters)¶
Use ports (interfaces) and adapters pattern.
- Pros: Maximum flexibility, very testable, clear boundaries
- Cons: More abstract, requires interfaces/protocols for every boundary, overhead for small project
- Reason for rejection: Too much ceremony for current needs; layered architecture provides similar benefits with less complexity
MVC Pattern¶
Adopt Model-View-Controller pattern.
- Pros: Well-known web pattern
- Cons: Designed for UI interactions, not for CLI command wrapper tools; controller/view distinction unclear for CLI
- Reason for rejection: Not a natural fit for CLI command wrapper tools
Future Direction¶
The layered architecture should remain stable through LLM integration. Potential triggers for revisiting:
- Infrastructure layer grows too large: If we add many infrastructure adapters (multiple LLM providers, various I/O sources), consider splitting into subdirectories (infrastructure/llm/, infrastructure/io/)
- Cross-cutting concerns emerge: If we need logging, metrics, or tracing across all layers, consider aspect-oriented patterns or middleware
- Domain logic becomes complex: If text processing logic grows significantly, consider splitting domain.py into multiple modules or introducing domain-driven design patterns
For now, this lightweight four-layer structure provides the right balance of organization and simplicity.