Clean Architecture, introduced by Robert C. Martin (Uncle Bob), is a software design philosophy that emphasizes separation of concerns and independence from frameworks, UI, and databases.
The domain layer contains entities with embedded business rules and validation:
domain/models/movie.py
from dataclasses import dataclass, fieldfrom typing import Optional, Listimport refrom domain.models.actor import Actor@dataclassclass Movie: """ Modelo de dominio que representa una película y valida su propia integridad. """ id: Optional[int] imdb_id: str title: str year: int rating: float duration_minutes: Optional[int] metascore: Optional[int] actors: List[Actor] = field(default_factory=list) def __post_init__(self): """Realiza validaciones en los datos después de que el objeto es creado.""" # Limpieza de datos self.title = self.title.strip() self.imdb_id = self.imdb_id.strip() # Reglas de validación if not re.match(r"^tt\d{7,}$", self.imdb_id): raise ValueError(f"IMDb ID inválido: '{self.imdb_id}'") if not self.title: raise ValueError("El título no puede estar vacío.") if not (1888 <= self.year <= 2030): raise ValueError(f"Año inválido: {self.year}") if not (0.0 <= self.rating <= 10.0): raise ValueError(f"Rating inválido: {self.rating}")
Notice how the Movie entity has zero dependencies on frameworks or infrastructure. It only knows about business rules.
Interfaces define contracts without implementation details:
domain/repositories/movie_repository.py
from abc import ABC, abstractmethodfrom typing import Optionalfrom domain.models.movie import Movieclass MovieRepository(ABC): """ Interfaz de repositorio para la entidad Movie. Define el contrato que deben cumplir las implementaciones. """ @abstractmethod def save(self, movie: Movie) -> Movie: """Guarda una película y retorna la entidad guardada.""" pass @abstractmethod def find_by_imdb_id(self, imdb_id: str) -> Optional[Movie]: """Busca una película por su ID de IMDb.""" pass
domain/interfaces/scraper_interface.py
from abc import ABC, abstractmethodfrom typing import Listfrom domain.models.movie import Movieclass ScraperInterface(ABC): """ Interfaz base para scrapers de películas. """ @abstractmethod def scrape(self) -> List[Movie]: """Ejecuta el proceso de scraping.""" pass
from concurrent.futures import ThreadPoolExecutorfrom typing import Listfrom domain.models.movie import Moviefrom domain.interfaces.use_case_interface import UseCaseInterfaceclass CompositeSaveMovieWithActorsUseCase(UseCaseInterface): """ Caso de uso compuesto que orquesta múltiples casos de uso de forma concurrente. """ def __init__(self, use_cases: List[UseCaseInterface]): self.use_cases = use_cases self.max_workers = len(use_cases) def execute(self, movie: Movie) -> None: """Ejecuta todos los casos de uso en paralelo.""" with ThreadPoolExecutor(max_workers=self.max_workers) as executor: list(executor.map(lambda uc: uc.execute(movie), self.use_cases))
The use case depends on UseCaseInterface (abstraction), not on concrete implementations. This follows the Dependency Inversion Principle.
# Test entities without any infrastructuredef test_movie_validation(): with pytest.raises(ValueError): Movie( id=None, imdb_id="invalid", # Invalid IMDb ID title="Test Movie", year=2024, rating=8.5, duration_minutes=120, metascore=85 )
# Test use case with mock repositoriesdef test_save_movie_use_case(): mock_repo = Mock(spec=MovieRepository) use_case = SaveMovieWithActorsPostgresUseCase( movie_repository=mock_repo, actor_repository=Mock(), movie_actor_repository=Mock() ) movie = Movie(...) use_case.execute(movie) mock_repo.save.assert_called_once()
# Test repository against real database (integration test)def test_postgres_repository(): conn = get_test_db_connection() repo = MoviePostgresRepository(conn) movie = Movie(...) saved_movie = repo.save(movie) assert saved_movie.id is not None
Clean Architecture enables isolated unit tests for domain and application layers, and integration tests for infrastructure.