Date: 2025-02-24
Status
Accepted
Context
As we migrate our existing Ruby-based ebook conversion system to Elixir, we need to establish a clear architectural approach that will:
- Accurately capture the complex domain logic of ebook creation, conversion, and validation
- Enable us to leverage Elixir/OTP's concurrency, fault tolerance, and distribution capabilities
- Support the multiple format generation pipeline with improved build management
- Facilitate clear boundaries between different system components
- Provide a model that can evolve as the domain requirements change
Our ebook conversion system encompasses complex workflows with various specialized tools (LaTeX, Calibre, etc.), quality validation, and build automation. The domain complexity extends beyond simple CRUD operations and involves intricate business rules around format conversion, metadata management, and quality control.
Decision
We will adopt Domain-Driven Design (DDD) principles in our Elixir replatforming effort, specifically:
-
Bounded Contexts: We will define clear bounded contexts for:
- Build Environment Management
- Workflow Automation
- Content Processing
- Format Generation
- Quality Control & Validation
- Metadata Management
- Audit System
-
Ubiquitous Language: We will establish a consistent vocabulary within each bounded context that reflects the domain concepts. This language will be used in code, documentation, and team communications.
-
Aggregates and Entities:
- Define key aggregates such as
Manuscript,Build,Workflow, andValidationReport - Identify entities with identity such as
Metadata,FormatOutput, andAuditLog - Use value objects for immutable concepts like
ValidationRule,FormatSpecification, andBuildEnvironment
- Define key aggregates such as
-
Domain Events: Leverage Elixir's process model and pub/sub capabilities to implement domain events for:
- Build state transitions
- Format generation completion
- Validation failures/successes
- Configuration changes
-
Repositories: Implement context-specific repositories to provide domain-focused data access
-
Services: Create domain services for operations that don't naturally belong to entities:
FormatConversionServiceValidationServiceBuildOptimizationService
-
Anti-corruption Layers: Create adapters for external tools (LaTeX, Calibre) to translate between their models and our domain model
Implementation Strategy
In Elixir, we'll implement DDD using:
-
Module structure that reflects bounded contexts:
lib/ elib/ build_manager/ workflow/ validation/ metadata/ format/ audit/ -
Structs and protocols for domain entities and value objects:
defmodule Codexis.Manuscript do @enforce_keys [:id, :content, :metadata] defstruct [:id, :content, :metadata, :status, artifacts: %{}] end -
GenServers and Supervisors for managing entity lifecycles and implementing domain services:
defmodule Codexis.BuildManager do use GenServer # Domain logic for build management end -
Registry and PubSub for implementing domain events:
defmodule Codexis.DomainEvents do def publish(event) do Phoenix.PubSub.broadcast(Codexis.PubSub, "domain_events", {:event, event}) end end -
Ecto schemas for persistence where appropriate, keeping domain logic separate:
defmodule Codexis.Metadata.Schema do use Ecto.Schema # Schema definitions end
Consequences
Positive
- Improved domain understanding: DDD will help the team develop a shared understanding of the complex ebook domain
- More maintainable code: Clear boundaries between contexts will make the system more modular
- Better alignment with Elixir philosophy: The actor model of Elixir naturally supports modeling domain entities as processes
- Enhanced testability: Domain-centric modules can be tested in isolation
- Future extensibility: New formats or workflow steps can be added with minimal changes to existing components
- Parallel development: Different bounded contexts can be developed by different team members concurrently
Negative
- Learning curve: Team members need to learn both Elixir and DDD principles
- Initial development overhead: Establishing the DDD structure requires more upfront design
- Potential over-engineering risk: Need to balance DDD purity with practical implementation
- Integration complexity: Managing interactions between bounded contexts adds complexity
Mitigation Strategies
- Schedule knowledge-sharing sessions on DDD and Elixir
- Start with a minimal viable DDD implementation and refine iteratively
- Regular architecture reviews to prevent overengineering
- Create clear interface contracts between bounded contexts
- Document the ubiquitous language in a project glossary
- Implement metrics to track performance impacts of the design
References
- Eric Evans, "Domain-Driven Design: Tackling Complexity in the Heart of Software"
- Vaughn Vernon, "Implementing Domain-Driven Design"
- José Valim, "Elixir in Action"
- "Designing Elixir Systems with OTP" by James Edward Gray II and Bruce A. Tate
- "The Clean Architecture" by Robert C. Martin