This document describes how the Open Resource Broker implements Clean Architecture principles, ensuring separation of concerns, testability, and maintainability.
Clean Architecture organizes code into layers with clear dependency rules:
- Dependencies point inward: Outer layers depend on inner layers, never the reverse
- Business logic is isolated: Core business rules are independent of external concerns
- Interfaces define contracts: Abstract interfaces separate layers
- Framework independence: Business logic doesn't depend on frameworks
The domain layer contains the core business logic and has no external dependencies.
src/domain/
Entities (Aggregates)
# src/domain/template/aggregate.py
class Template(BaseModel):
"""Template configuration representing VM template."""
template_id: str
max_number: int
attributes: Dict[str, Any]
def validate_configuration(self) -> bool:
"""Business rule: validate template configuration."""
# Core business logic hereValue Objects
# src/domain/machine/machine_status.py
class MachineStatus(Enum):
"""Machine status value object."""
PENDING = "pending"
RUNNING = "running"
TERMINATED = "terminated"Domain Services
# src/domain/template/ami_resolver.py
class AMIResolver:
"""Domain service for AMI resolution logic."""
def resolve_ami_id(self, ami_reference: str) -> str:
"""Business logic for AMI resolution."""Repository Interfaces
# src/domain/template/repository.py
class TemplateRepository(ABC):
"""Abstract repository interface."""
@abstractmethod
async def get_by_id(self, template_id: str) -> Optional[Template]:
"""Get template by ID."""- No external dependencies: Pure business logic
- Framework independent: No FastAPI, SQLAlchemy, or AWS SDK dependencies
- Testable: Easy unit testing without external systems
- Stable: Changes rarely, only when business rules change
The application layer orchestrates domain objects and implements use cases.
src/application/
Application Services
# src/application/service.py
@injectable
class ApplicationService:
"""Main application orchestrator."""
def __init__(self,
command_bus: CommandBus,
query_bus: QueryBus,
provider_context: ProviderContext):
# Dependencies injected, not createdCommand Handlers (CQRS)
# src/application/commands/template_handlers.py
class GetTemplatesHandler:
"""Handle template retrieval commands."""
def handle(self, command: GetTemplatesCommand) -> List[Template]:
# Coordinate domain objectsQuery Handlers (CQRS)
# src/application/queries/handlers.py
class TemplateQueryHandler:
"""Handle template queries."""
def handle(self, query: TemplateQuery) -> TemplateResponse:
# Process queries using domain objectsData Transfer Objects
# src/application/dto/commands.py
class CreateRequestCommand:
"""Command for creating requests."""
template_id: str
max_number: int- Depends on domain layer: Uses domain entities and services
- Independent of infrastructure: No database or external service dependencies
- Use case focused: Each handler represents a specific use case
- Testable: Can be tested with mock repositories
The infrastructure layer implements external concerns and technical details.
src/infrastructure/src/providers/
Repository Implementations
# src/infrastructure/storage/repositories/template_repository.py
class TemplateRepositoryImpl(TemplateRepository):
"""Template repository implementation."""
async def get_by_id(self, template_id: str) -> Optional[Template]:
# Implementation delegates to configuration managerExternal Service Adapters
# src/providers/aws/managers/aws_instance_manager.py
@injectable
class AWSInstanceManager:
"""AWS-specific instance management."""
def __init__(self, aws_client: AWSClient, logger: LoggingPort):
# Infrastructure dependenciesDependency Injection Container
# src/infrastructure/di/container.py
class DIContainer:
"""Dependency injection container."""
def register_singleton(self, interface: Type, implementation: Type):
# DI container implementationConfiguration Management
# src/infrastructure/config/manager.py
class ConfigurationManager:
"""Configuration management implementation."""- Implements interfaces: Implements domain and application interfaces
- External dependencies: Database, cloud services, frameworks
- Technology specific: Contains technology-specific code
- Replaceable: Can be replaced without affecting business logic
The interface layer provides external access points to the system.
src/interface/src/api/src/cli/
CLI Interface
# src/cli/main.py
def main():
"""CLI entry point."""
# Parse arguments
# Call application services
# Format outputREST API Interface
# src/api/routers/templates.py
@router.get("/templates")
async def get_templates():
"""REST API endpoint."""
# HTTP-specific handling
# Call application services
# Return JSON responseInterface Command Handlers
# src/interface/template_command_handlers.py
class TemplateCommandHandler:
"""Handle CLI template commands."""
def handle_list_templates(self, args):
# CLI-specific processing
# Call application layer- External facing: Direct interaction with users/systems
- Framework dependent: Uses FastAPI, Click, etc.
- Format specific: Handles JSON, CLI output, etc.
- Thin layer: Minimal logic, delegates to application layer
Interface Layer -> Application Layer -> Domain Layer
^ ^ ^
Infrastructure Layer ---- -> |
^ |
External Systems ---------------------- ->
Repository Pattern
# Domain layer defines interface
class TemplateRepository(ABC):
@abstractmethod
async def get_by_id(self, template_id: str) -> Optional[Template]:
pass
# Infrastructure layer implements interface
class TemplateRepositoryImpl(TemplateRepository):
async def get_by_id(self, template_id: str) -> Optional[Template]:
# Implementation using configuration managerProvider Strategy Pattern
# Domain layer defines interface
class ProviderStrategy(ABC):
@abstractmethod
async def provision_instances(self, request: Request) -> List[Machine]:
pass
# Infrastructure layer implements interface
class AWSProviderStrategy(ProviderStrategy):
async def provision_instances(self, request: Request) -> List[Machine]:
# AWS implementationUnit Testing Domain Logic
def test_template_validation():
"""Test domain logic without external dependencies."""
template = Template(template_id="test", max_number=5, attributes={})
assert template.validate_configuration() == TrueIntegration Testing Application Layer
def test_create_request_handler():
"""Test application logic with mock repositories."""
mock_repo = Mock(spec=RequestRepository)
handler = CreateRequestHandler(mock_repo)
# Test without real databaseBusiness Logic Isolation
- Domain logic doesn't depend on FastAPI, SQLAlchemy, or AWS SDK
- Can switch from FastAPI to Flask without changing business logic
- Can switch from JSON to SQL storage without changing domain
Technology Flexibility
- Infrastructure implementations can be replaced
- New providers can be added without changing core logic
- Different storage backends can be used
Clear Boundaries
- Each layer has specific responsibilities
- Changes in one layer don't affect others
- Easy to understand and modify
Separation of Concerns
- Business logic separated from technical concerns
- External dependencies isolated in infrastructure layer
- Interface concerns separated from business logic
Constructor Injection
@injectable
class ApplicationService:
def __init__(self,
template_repo: TemplateRepository, # Abstraction, not implementation
logger: LoggingPort): # Abstraction, not implementation
self._template_repo = template_repo
self._logger = loggerInterface Segregation
# Small, focused interfaces
class LoggingPort(ABC):
@abstractmethod
def info(self, message: str) -> None:
pass
class ConfigurationPort(ABC):
@abstractmethod
def get(self, key: str) -> Any:
passCommand Side (Write)
class CreateRequestCommand:
template_id: str
max_number: int
class CreateRequestHandler:
def handle(self, command: CreateRequestCommand) -> RequestId:
# Handle state changesQuery Side (Read)
class GetTemplatesQuery:
filters: Optional[Dict[str, Any]] = None
class GetTemplatesHandler:
def handle(self, query: GetTemplatesQuery) -> List[Template]:
# Handle data retrievalAbstract Repository
class TemplateRepository(ABC):
@abstractmethod
async def get_all(self) -> List[Template]:
pass
@abstractmethod
async def get_by_id(self, template_id: str) -> Optional[Template]:
passConcrete Implementation
class TemplateRepositoryImpl(TemplateRepository):
def __init__(self, config_manager: ConfigurationManager, logger: LoggingPort):
self._config_manager = config_manager
self._logger = logger
async def get_all(self) -> List[Template]:
# Implementation using configuration managerDomain Layer Testing
- Pure unit tests
- No external dependencies
- Fast execution
- High coverage
Application Layer Testing
- Unit tests with mocked repositories
- Integration tests with real repositories
- Use case validation
Infrastructure Layer Testing
- Integration tests with real external systems
- Contract tests for interfaces
- Performance tests
Interface Layer Testing
- End-to-end tests
- API contract tests
- CLI behavior tests
E2E Tests (Interface Layer)
/\
/ \
Integration Tests (Application + Infrastructure)
/\ /\
/ \ / \
Unit Tests (Domain + Application)
This Clean Architecture implementation ensures that the Open Resource Broker maintains clear separation of concerns, high testability, and flexibility for future changes while adhering to established software engineering principles.