StoryMatrix system design answers how domain rules stay independent while application workflows connect to infrastructure and CrewAI orchestration.
🧱 Layers and dependency rule
| Layer | Responsibility | Allowed dependencies |
|---|---|---|
| Domain | Entities, value objects, enums, domain services, exceptions, and repository contracts | Nothing outside the domain and standard-library dependencies (src/storymatrix/domain/) |
| Application | Use cases, production stages, DTOs, service ports, and orchestration of business workflows | Domain only (src/storymatrix/application/) |
| Infrastructure | Database repositories, external-provider adapters, media services, and concrete service implementations | Domain and Application (src/storymatrix/infrastructure/) |
| Crew | LLM-oriented agents, tasks, prompts, schemas, and crew runners | Orchestration that supplies application workflows (src/storymatrix/crew/) |
The dependency direction keeps domain policy independent from providers and runtime frameworks. Crew is the LLM orchestration layer rather than a replacement for domain or application policy.
🔌 Ports and adapters
Application ports define capabilities consumed by use cases and stages in application/interfaces/services.py, including story orchestration, timeline generation, audio generation and production, image generation, voice services, and other application-facing services. Infrastructure supplies concrete adapters and services for those ports in infrastructure/adapters/ and infrastructure/services/.
Domain ports define persistence contracts in domain/interfaces/repositories.py. Infrastructure implements those contracts with SQLite repositories and in-memory repositories in infrastructure/repositories/. The application calls abstractions; the container selects concrete implementations.
🧩 Dependency injection
infrastructure/container.py defines ServiceContainer as a dependency-injector declarative container. Providers include Singleton, Factory, Selector, Callable, and Configuration providers. Selector keys choose adapters from configuration and local-development gates.
The container keeps heavy imports inside provider factories instead of importing them during module startup. A concrete pattern is the montage factory:
def ffmpeg_montage_service_factory(config, temp_dir):
from storymatrix.infrastructure.services.ffmpeg_montage import FFmpegMontageService
from storymatrix.config.models import MontageConfig
montage_config_dict = _safe_config_get(config, "services.montage", {})
ffmpeg_path = _safe_config_get(config, "paths.ffmpeg", "/usr/bin/ffmpeg")
ffprobe_path = _safe_config_get(config, "paths.ffprobe", "/usr/bin/ffprobe")
montage_config = MontageConfig(**montage_config_dict)
return FFmpegMontageService(
config=montage_config,
ffmpeg_path=ffmpeg_path,
ffprobe_path=ffprobe_path,
)The same pattern appears in factories for image, TTS, SFX, music, and audio-search adapters. Lazy inner imports prevent startup crashes from optional C-extension stacks such as torch and pydub on CPU-only hosts, while allowing the selected provider to load when its provider is constructed (src/storymatrix/infrastructure/container.py).
🕸️ Story aggregate
A Story owns ordered Scene objects; each scene owns ordered Segment objects, while characters connect to dialogue segments through character identity (src/storymatrix/domain/entities/story.py, src/storymatrix/domain/entities/scene.py, src/storymatrix/domain/entities/segment.py).
flowchart TD Story --> Scene Scene --> Segment Story --> Character Segment -->|dialogue reference| Character
The segment union contains four subtypes—narration, dialogue, sound effects, and music. Image content is a separate entity rather than a segment subtype (src/storymatrix/domain/entities/segment.py, src/storymatrix/domain/entities/image.py).
🗄️ SQLite persistence
| Store | Content | Repository wiring |
|---|---|---|
data/storymatrix.db | Stories, characters, scenes, segments, their relationships, and media-asset metadata | SQLiteStoryRepository, SQLiteCharacterRepository, and FileSystemMediaAssetRepository (the latter uses its data/storymatrix.db config default) (src/storymatrix/infrastructure/database/models.py, src/storymatrix/infrastructure/container.py, src/storymatrix/config/models.py) |
data/audio_metadata.db | Character-to-voice mappings | SQLiteCharacterVoiceMappingRepository (src/storymatrix/infrastructure/database/models.py, src/storymatrix/infrastructure/container.py) |
The content schema uses stories, characters, scenes, segments, and character_voice_mappings tables; segment rows use a single-table polymorphic shape keyed by segment_type (src/storymatrix/infrastructure/database/models.py).
🛡️ Domain isolation guard
tests/unit/domain/test_domain_isolation.py parses every domain Python file and rejects imports whose module name starts with any of these exact prefixes:
storymatrix.config
storymatrix.application
storymatrix.infrastructure
storymatrix.crewThe same test checks that constructing StoryTemplates does not call builtins.open, keeping domain template objects free from direct filesystem access (tests/unit/domain/test_domain_isolation.py).