Skip to content

Separation of Concerns Implementation

This document describes how the Open Host Factory Plugin implements Separation of Concerns (SoC), demonstrating clear boundaries between different responsibilities and how the Single Responsibility Principle (SRP) is applied throughout the architecture.

Separation of Concerns Overview

Separation of Concerns is a design principle that divides a system into distinct sections, each addressing a separate concern. In the plugin, this is achieved through:

  • Layer separation: Clear boundaries between domain, application, infrastructure, and interface layers
  • Component isolation: Each component has a single, well-defined responsibility
  • Interface segregation: Focused interfaces that separate different concerns
  • Dependency direction: Dependencies flow in one direction, maintaining separation

Architectural Layer Separation

Domain Layer Concerns

The domain layer is concerned only with business logic and rules:

# src/domain/template/aggregate.py
class Template(AggregateRoot):
    """Domain concern: Template business logic and rules."""

    template_id: str
    max_number: int
    attributes: Dict[str, Any]

    def validate_configuration(self) -> bool:
        """Business concern: Template validation rules."""
        # Only contains business validation logic
        # No infrastructure, persistence, or UI concerns
        return (
            self._validate_max_number() and
            self._validate_required_attributes() and
            self._validate_business_constraints()
        )

    def _validate_max_number(self) -> bool:
        """Business rule: max_number constraints."""
        return 1 <= self.max_number <= 1000

    def _validate_required_attributes(self) -> bool:
        """Business rule: required attribute validation."""
        required_attrs = ['vm_type', 'image_id']
        return all(attr in self.attributes for attr in required_attrs)

    def calculate_estimated_cost(self) -> float:
        """Business concern: Cost calculation logic."""
        # Pure business logic - no external dependencies
        base_cost = self.attributes.get('hourly_rate', 0.10)
        return base_cost * self.max_number

# src/domain/request/aggregate.py
class Request(AggregateRoot):
    """Domain concern: Request business logic."""

    def can_be_fulfilled(self, available_capacity: int) -> bool:
        """Business concern: Fulfillment logic."""
        # Pure business logic
        return available_capacity >= self.max_number

    def transition_to_status(self, new_status: RequestStatus) -> None:
        """Business concern: State transition rules."""
        # Business rules for valid state transitions
        valid_transitions = {
            RequestStatus.PENDING: [RequestStatus.PROCESSING, RequestStatus.CANCELLED],
            RequestStatus.PROCESSING: [RequestStatus.COMPLETED, RequestStatus.FAILED],
            RequestStatus.COMPLETED: [],
            RequestStatus.FAILED: [RequestStatus.PENDING],
            RequestStatus.CANCELLED: []
        }

        if new_status not in valid_transitions.get(self.status, []):
            raise InvalidStateTransitionError(f"Cannot transition from {self.status} to {new_status}")

        self.status = new_status
        self.add_domain_event(RequestStatusChangedEvent(self.id, new_status))

Application Layer Concerns

The application layer is concerned with use cases and orchestration:

# src/application/commands/request_handlers.py
class CreateRequestHandler:
    """Application concern: Request creation use case."""

    def __init__(self,
                 request_repo: RequestRepository,
                 template_repo: TemplateRepository,
                 logger: LoggingPort):
        # Application layer dependencies - no infrastructure details
        self._request_repo = request_repo
        self._template_repo = template_repo
        self._logger = logger

    async def handle(self, command: CreateRequestCommand) -> str:
        """Application concern: Orchestrate request creation."""
        self._logger.info(f"Creating request for template: {command.template_id}")

        # Application logic: validate template exists
        template = await self._template_repo.get_by_id(command.template_id)
        if not template:
            raise TemplateNotFoundError(command.template_id)

        # Application logic: validate template can fulfill request
        if not template.can_fulfill_request(command.max_number):
            raise InsufficientCapacityError(f"Template cannot fulfill {command.max_number} instances")

        # Domain logic: create request
        request = Request.create(
            template_id=command.template_id,
            max_number=command.max_number,
            attributes=command.attributes
        )

        # Application logic: persist request
        await self._request_repo.save(request)

        self._logger.info(f"Request created: {request.id}")
        return request.id

# src/application/queries/template_handlers.py
class GetTemplatesHandler:
    """Application concern: Template retrieval use case."""

    def __init__(self,
                 template_repo: TemplateRepository,
                 logger: LoggingPort):
        self._template_repo = template_repo
        self._logger = logger

    async def handle(self, query: GetTemplatesQuery) -> List[TemplateResponse]:
        """Application concern: Orchestrate template retrieval."""
        self._logger.info("Retrieving templates")

        # Application logic: retrieve templates with filters
        templates = await self._template_repo.get_all(
            filters=query.filters,
            limit=query.limit,
            offset=query.offset
        )

        # Application logic: convert to response DTOs
        responses = []
        for template in templates:
            response = TemplateResponse(
                template_id=template.template_id,
                max_number=template.max_number,
                attributes=template.attributes,
                estimated_cost=template.calculate_estimated_cost()  # Uses domain logic
            )
            responses.append(response)

        return responses

Infrastructure Layer Concerns

The infrastructure layer is concerned with external systems and technical implementation:

# src/infrastructure/persistence/dynamodb/template_repository.py
class DynamoDBTemplateRepository(TemplateRepository):
    """Infrastructure concern: DynamoDB persistence implementation."""

    def __init__(self, table_name: str, region: str, logger: LoggingPort):
        # Infrastructure concerns: AWS configuration
        self._table_name = table_name
        self._region = region
        self._logger = logger
        self._dynamodb = boto3.resource('dynamodb', region_name=region)
        self._table = self._dynamodb.Table(table_name)

    async def get_by_id(self, template_id: str) -> Optional[Template]:
        """Infrastructure concern: DynamoDB data retrieval."""
        try:
            # Infrastructure logic: DynamoDB operations
            response = self._table.get_item(Key={'template_id': template_id})

            if 'Item' not in response:
                return None

            # Infrastructure concern: Data transformation
            return self._item_to_domain_object(response['Item'])

        except ClientError as e:
            # Infrastructure concern: AWS error handling
            self._logger.error(f"DynamoDB error retrieving template {template_id}: {e}")
            raise RepositoryError(f"Failed to retrieve template: {e}")

    def _item_to_domain_object(self, item: Dict[str, Any]) -> Template:
        """Infrastructure concern: DynamoDB to domain object conversion."""
        return Template(
            template_id=item['template_id'],
            max_number=int(item['max_number']),
            attributes=item.get('attributes', {})
        )

# src/providers/aws/infrastructure/aws_client.py
class AWSClient:
    """Infrastructure concern: AWS service client management."""

    def __init__(self, config: AWSProviderConfig, logger: LoggingPort):
        # Infrastructure concerns: AWS configuration and client management
        self._config = config
        self._logger = logger
        self._clients: Dict[str, Any] = {}
        self._session = self._create_session()

    def get_client(self, service_name: str) -> Any:
        """Infrastructure concern: AWS client creation and caching."""
        if service_name not in self._clients:
            # Infrastructure logic: AWS client creation
            self._clients[service_name] = self._session.client(
                service_name,
                region_name=self._config.region,
                config=self._get_client_config()
            )
            self._logger.debug(f"Created AWS {service_name} client")

        return self._clients[service_name]

    def _create_session(self) -> boto3.Session:
        """Infrastructure concern: AWS session management."""
        if self._config.profile:
            return boto3.Session(profile_name=self._config.profile)
        else:
            return boto3.Session()

Interface Layer Concerns

The interface layer is concerned with external communication and presentation:

# src/api/routers/templates.py
@router.get("/templates")
async def get_templates(
    limit: Optional[int] = Query(None, ge=1, le=100),
    offset: Optional[int] = Query(None, ge=0),
    provider_type: Optional[str] = Query(None),
    app_service: ApplicationService = Depends(get_application_service)
) -> List[TemplateResponse]:
    """Interface concern: HTTP API endpoint for template retrieval."""

    try:
        # Interface concern: HTTP parameter validation and conversion
        filters = {}
        if provider_type:
            filters['provider_type'] = provider_type

        # Interface concern: Call application layer
        query = GetTemplatesQuery(
            filters=filters,
            limit=limit,
            offset=offset
        )

        templates = await app_service.get_templates(query)

        # Interface concern: HTTP response formatting
        return [TemplateResponse.model_validate(template) for template in templates]

    except TemplateNotFoundError as e:
        # Interface concern: HTTP error handling
        raise HTTPException(status_code=404, detail=str(e))
    except ValidationError as e:
        # Interface concern: HTTP validation error handling
        raise HTTPException(status_code=400, detail=str(e))

# src/cli/formatters.py
class TemplateFormatter:
    """Interface concern: CLI output formatting."""

    def __init__(self, field_mapper: FieldMapper):
        self._field_mapper = field_mapper

    def format_templates(self, 
                        templates: List[Dict[str, Any]], 
                        output_format: str,
                        long_format: bool = False) -> str:
        """Interface concern: CLI output formatting logic."""

        if output_format == "table":
            return self._format_as_table(templates, long_format)
        elif output_format == "json":
            return self._format_as_json(templates, long_format)
        elif output_format == "yaml":
            return self._format_as_yaml(templates, long_format)
        else:
            return self._format_as_list(templates, long_format)

    def _format_as_table(self, templates: List[Dict[str, Any]], long_format: bool) -> str:
        """Interface concern: Table formatting logic."""
        # CLI-specific formatting logic
        # No business logic or infrastructure concerns
        pass

Component Responsibility Separation

Configuration Management Separation

# src/config/manager.py
class ConfigurationManager:
    """Single concern: Configuration management."""

    def __init__(self, config_path: Optional[str] = None):
        # Only concerned with configuration loading and management
        self._config_data = {}
        self._config_path = config_path
        self._load_configuration()

    def get(self, key: str, default: Any = None) -> Any:
        """Single concern: Configuration value retrieval."""
        # Only handles configuration access logic
        pass

    def _load_configuration(self) -> None:
        """Single concern: Configuration loading."""
        # Only handles file loading and parsing
        pass

# src/config/validation/validator.py
class ConfigurationValidator:
    """Single concern: Configuration validation."""

    def validate_provider_config(self, config: Dict[str, Any]) -> ValidationResult:
        """Single concern: Provider configuration validation."""
        # Only handles validation logic
        pass

    def validate_storage_config(self, config: Dict[str, Any]) -> ValidationResult:
        """Single concern: Storage configuration validation."""
        # Only handles storage validation
        pass

# src/config/schemas/provider_schema.py
class ProviderConfigSchema:
    """Single concern: Provider configuration schema definition."""

    # Only defines configuration structure
    # No validation or loading logic
    pass

Logging Responsibility Separation

# src/infrastructure/logging/logger.py
def get_logger(name: str) -> logging.Logger:
    """Single concern: Logger creation."""
    # Only handles logger setup and configuration
    pass

# src/infrastructure/adapters/logging_adapter.py
class LoggingAdapter(LoggingPort):
    """Single concern: Logging port implementation."""

    def info(self, message: str, **kwargs) -> None:
        """Single concern: Info level logging."""
        # Only handles logging operation
        pass

# src/infrastructure/logging/formatters.py
class StructuredFormatter(logging.Formatter):
    """Single concern: Log message formatting."""

    def format(self, record: logging.LogRecord) -> str:
        """Single concern: Format log records."""
        # Only handles log formatting
        pass

Error Handling Separation

# src/domain/base/exceptions.py
class DomainException(Exception):
    """Domain concern: Domain-specific exceptions."""
    pass

class TemplateValidationError(DomainException):
    """Domain concern: Template validation errors."""
    pass

# src/infrastructure/error/exceptions.py
class InfrastructureException(Exception):
    """Infrastructure concern: Infrastructure-specific exceptions."""
    pass

class RepositoryError(InfrastructureException):
    """Infrastructure concern: Repository operation errors."""
    pass

# src/infrastructure/error/exception_handler.py
class ExceptionHandler:
    """Single concern: Exception handling and conversion."""

    def handle_domain_exception(self, ex: DomainException) -> ErrorResponse:
        """Single concern: Domain exception handling."""
        # Only handles domain exception conversion
        pass

    def handle_infrastructure_exception(self, ex: InfrastructureException) -> ErrorResponse:
        """Single concern: Infrastructure exception handling."""
        # Only handles infrastructure exception conversion
        pass

Interface Segregation for Separation

Port Segregation

# Separate ports for different concerns

# src/domain/base/ports/logging_port.py
class LoggingPort(ABC):
    """Single concern: Logging operations."""

    @abstractmethod
    def info(self, message: str) -> None:
        pass

# src/domain/base/ports/configuration_port.py
class ConfigurationPort(ABC):
    """Single concern: Configuration access."""

    @abstractmethod
    def get(self, key: str, default: Any = None) -> Any:
        pass

# src/domain/base/ports/container_port.py
class ContainerPort(ABC):
    """Single concern: Dependency injection."""

    @abstractmethod
    def get(self, interface: Type[T]) -> T:
        pass

# Components depend only on their specific concerns
class ApplicationService:
    def __init__(self,
                 logger: LoggingPort,        # Only logging concern
                 config: ConfigurationPort,  # Only configuration concern
                 container: ContainerPort):  # Only DI concern
        # Each dependency addresses a single concern
        pass

Repository Interface Segregation

# Separate interfaces for different data access concerns

# src/domain/base/repository_ports.py
class Reader(ABC, Generic[T]):
    """Single concern: Read operations."""

    @abstractmethod
    async def get_by_id(self, entity_id: str) -> Optional[T]:
        pass

    @abstractmethod
    async def get_all(self) -> List[T]:
        pass

class Writer(ABC, Generic[T]):
    """Single concern: Write operations."""

    @abstractmethod
    async def save(self, entity: T) -> None:
        pass

    @abstractmethod
    async def delete(self, entity_id: str) -> bool:
        pass

class Searcher(ABC, Generic[T]):
    """Single concern: Search operations."""

    @abstractmethod
    async def find_by_criteria(self, criteria: Dict[str, Any]) -> List[T]:
        pass

# Components depend only on needed concerns
class TemplateQueryHandler:
    def __init__(self, reader: Reader[Template]):  # Only needs read access
        self._reader = reader

class TemplateCommandHandler:
    def __init__(self, writer: Writer[Template]):  # Only needs write access
        self._writer = writer

class TemplateSearchHandler:
    def __init__(self, searcher: Searcher[Template]):  # Only needs search access
        self._searcher = searcher

Benefits of Separation of Concerns

Maintainability

  • Isolated changes: Changes to one concern don't affect others
  • Clear boundaries: Easy to understand what each component does
  • Focused testing: Each concern can be tested independently

Flexibility

  • Independent evolution: Different concerns can evolve at different rates
  • Technology substitution: Infrastructure concerns can be replaced without affecting business logic
  • Parallel development: Different teams can work on different concerns

Reusability

  • Component reuse: Well-separated components can be reused in different contexts
  • Interface reuse: Clean interfaces can be implemented by different components
  • Logic reuse: Business logic is independent of infrastructure and can be reused

Testability

  • Unit testing: Each concern can be unit tested in isolation
  • Mock substitution: Interfaces enable easy mocking of dependencies
  • Integration testing: Different concerns can be tested together systematically

Code Quality

  • Single responsibility: Each component has one reason to change
  • Reduced complexity: Separation reduces overall system complexity
  • Better abstraction: Clear separation leads to better abstractions

The Separation of Concerns implementation in the Open Host Factory Plugin creates a well-structured, maintainable system where each component has a clear, single responsibility and dependencies flow in the correct direction according to Clean Architecture principles.