Architecture & Design
SpecifyX is built with a service-oriented architecture that prioritizes modularity, testability, and extensibility. This document explains the architectural decisions, design patterns, and engineering philosophy behind the project.
Core Architectural Principles
1. Service-Oriented Architecture (SOA)
SpecifyX adopts a service-oriented approach where each major functionality is encapsulated in dedicated services:
services/
├── config_service/ # Configuration management
├── template_service/ # Jinja2 template rendering
├── project_manager/ # Project orchestration
├── git_service/ # Git operations
├── download_service/ # GitHub/external downloads
├── script_discovery_service/ # Script finding and execution
├── script_execution_service/ # Cross-platform script running
├── update_service/ # Version checking and updates
├── update_installer/ # Installation management
└── version_checker/ # Version comparison utilities
Benefits:
- Separation of Concerns: Each service has a single, well-defined responsibility
- Testability: Services can be independently tested with mocked dependencies
- Extensibility: New services can be added without modifying existing code
- Maintainability: Clear boundaries make code easier to understand and modify
2. Abstract Base Classes (ABC) Pattern
All services implement abstract base classes to ensure consistent interfaces:
class TemplateService(ABC):
@abstractmethod
def render_template(self, template_name: str, context: TemplateContext) -> str:
pass
@abstractmethod
def load_template_package(self, ai_assistant: str, template_dir: Path) -> bool:
pass
Benefits:
- Interface Consistency: All implementations must follow the same contract
- Type Safety: Full type annotations ensure compile-time guarantees
- Polymorphism: Services can be swapped based on runtime conditions
- Testing: Easy to create mock implementations for testing
3. Dependency Injection
Services accept dependencies through constructor injection:
class ProjectManager:
def __init__(
self,
config_service: Optional[ConfigService] = None,
git_service: Optional[GitService] = None,
template_service: Optional[TemplateService] = None,
):
# Use provided services or create defaults
self._config_service = config_service or TomlConfigService()
self._git_service = git_service or CommandLineGitService()
self._template_service = template_service or JinjaTemplateService()
Benefits:
- Testability: Easy to inject mock services for testing
- Flexibility: Different implementations can be used in different contexts
- Loose Coupling: Services don't depend on concrete implementations
Data Models & Configuration
Configuration Hierarchy
SpecifyX uses a three-tier configuration system:
Configuration Hierarchy:
├── 1. Built-in Defaults (models/defaults/)
├── 2. Global User Config (~/.config/specifyx/config.toml)
└── 3. Project Config (project/.specify/config.toml)
Resolution Order: Project → Global → Defaults
Dataclass-Based Models
All configuration uses type-safe dataclasses with TOML serialization:
@dataclass
class ProjectConfig:
name: str
branch_naming: BranchNamingConfig = field(default_factory=BranchNamingConfig)
template_settings: TemplateConfig = field(default_factory=TemplateConfig)
created_at: Optional[datetime] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for TOML serialization"""
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "ProjectConfig":
"""Create instance from dictionary (TOML deserialization)"""
Benefits:
- Type Safety: All configuration is strongly typed
- Validation: Invalid configurations are caught at runtime
- Serialization: Clean conversion to/from TOML format
- IDE Support: Full autocomplete and type checking
Template System Architecture
Jinja2-Based Template Engine
SpecifyX uses a sophisticated template system built on Jinja2:
Template Flow:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Template Source │ -> │ Jinja2 Processing│ -> │ Rendered Output │
│ (.j2 files) │ │ (with context) │ │ (final files) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
Configurable Folder Mappings
Templates are organized using configurable folder mappings:
@dataclass(frozen=True)
class TemplateFolderMapping:
source: str # Source folder in templates/
target_pattern: str # Target pattern with variables
render: bool # Whether to render .j2 files
executable_extensions: List[str] # Files to make executable
Example Mapping:
TemplateFolderMapping(
source="scripts",
target_pattern=".specify/scripts",
render=True,
executable_extensions=[".py", ".sh", ".bat"]
)
AI-Aware Templates
Templates contain conditional logic for different AI assistants:
{% if ai_assistant == "claude" %}
# Claude-specific content
{% elif ai_assistant == "gemini" %}
# Gemini-specific content
{% else %}
# Generic fallback content
{% endif %}
Benefits:
- AI Agnostic: Same templates work with different AI assistants
- Customization: AI-specific optimizations where needed
- Fallbacks: Graceful degradation for unknown AI assistants
Cross-Platform Considerations
Path Handling
All path operations use pathlib.Path
for cross-platform compatibility:
# Good: Cross-platform path handling
project_path = Path.cwd() / project_name
config_file = project_path / ".specify" / "config.toml"
# Bad: Platform-specific separators
config_file = f"{project_name}/.specify/config.toml" # Unix-only
File Permissions
Executable permissions are set appropriately per platform:
if platform.system() != "Windows":
# Set executable permissions on Unix-like systems
script_path.chmod(EXECUTABLE_PERMISSIONS)
Error Handling Strategy
Validation-First Approach
All user inputs are validated before processing:
def validate_project_name(self, name: str) -> tuple[bool, Optional[str]]:
"""Validate project name using Validators infrastructure"""
try:
Validators.project_name(name)
return True, None
except ValidationError as e:
return False, str(e)
Graceful Degradation
Operations fail gracefully with meaningful error messages:
try:
result = self._template_service.render_template(template, context)
return RenderResult(success=True, content=result)
except TemplateNotFound:
return RenderResult(
success=False,
error_message=f"Template not found: {template}"
)
Performance Considerations
Lazy Loading
Services are instantiated only when needed:
@property
def git_service(self) -> GitService:
"""Lazy initialization of git service"""
if self._git_service is None:
self._git_service = CommandLineGitService()
return self._git_service
Minimal Dependencies
Core functionality has minimal external dependencies:
- typer: CLI framework
- rich: Terminal output formatting
- jinja2: Template engine
- httpx: HTTP client for updates
Testing Architecture
Contract Testing
Each service has contract tests that verify the ABC interface:
class TestTemplateServiceContract:
"""Test that all TemplateService implementations follow the contract"""
def test_render_template_contract(self, template_service: TemplateService):
# Test the contract requirements
Integration Testing
Full workflow tests verify service integration:
def test_project_initialization_workflow():
"""Test complete project initialization workflow"""
project_manager = ProjectManager()
result = project_manager.initialize_project(options)
assert result.success
Mock Testing
Services are tested in isolation using mocks:
def test_project_manager_with_mocked_services():
mock_config = Mock(spec=ConfigService)
mock_git = Mock(spec=GitService)
manager = ProjectManager(
config_service=mock_config,
git_service=mock_git
)
Conclusion
SpecifyX's architecture balances simplicity with extensibility, providing a solid foundation for spec-driven development workflows. The service-oriented design ensures maintainability while the abstract base classes guarantee consistency and testability.
The architecture supports the core philosophy of making spec-driven development accessible and powerful through clean abstractions, type safety, and cross-platform compatibility.