Date: 2025-03-01
Status
Accepted
Context
As we migrate our ebook conversion system from Ruby to Elixir, we need to establish a clear module structure that:
- Aligns with our Domain-Driven Design approach (ADR-0011)
- Leverages Elixir's functional paradigm effectively
- Provides clear boundaries between different system components
- Facilitates testing and maintenance
- Supports our supervision strategy (ADR-0021)
Our current Ruby implementation is organized around classes with inheritance and mixins, which doesn't translate directly to Elixir's functional approach. We need a module structure that preserves domain knowledge while embracing Elixir idioms.
Decision
We will organize our Elixir codebase using a bounded context approach with the following structure:
1. Top-Level Organization
lib/
├── book_processing.ex # Main application module
├── book_processing/
│ ├── application.ex # OTP Application
│ ├── configuration/ # Configuration context
│ ├── conversion/ # Conversion context
│ ├── validation/ # Validation context
│ ├── build/ # Build context
│ ├── error_handling/ # Error handling context
│ ├── telemetry/ # Telemetry context
│ └── utilities/ # Shared utilities
2. Context-Specific Structure
Each bounded context will follow a consistent internal structure:
Configuration Context
configuration/
├── supervisor.ex # Context supervisor
├── manager.ex # Configuration manager
├── path_resolver.ex # Path resolution
├── environment.ex # Environment detection
└── schemas/ # Configuration schemas
├── build_config.ex
├── format_config.ex
└── validation_config.ex
Conversion Context
conversion/
├── supervisor.ex # Context supervisor
├── latex_converter.ex # LaTeX conversion
├── docx_converter.ex # DOCX conversion
├── pandoc_converter.ex # Pandoc operations
├── format_registry.ex # Format registry
└── adapters/ # External tool adapters
├── pandoc_adapter.ex
├── calibre_adapter.ex
└── tex4ebook_adapter.ex
Validation Context
validation/
├── supervisor.ex # Context supervisor
├── coordinator.ex # Validation coordinator
├── file_validator.ex # File validation
├── epub_validator.ex # EPUB validation
├── pdf_validator.ex # PDF validation
├── metadata_validator.ex # Metadata validation
└── rules/ # Validation rules
├── epub_rules.ex
├── pdf_rules.ex
└── metadata_rules.ex
Build Context
build/
├── supervisor.ex # Context supervisor
├── coordinator.ex # Build coordinator
├── format_builder.ex # Format builder
├── output_manager.ex # Output management
└── formats/ # Format-specific builders
├── pdf_builder.ex
├── epub_builder.ex
├── mobi_builder.ex
└── html_builder.ex
3. Module Naming Conventions
We will follow these naming conventions:
- Context modules:
BookProcessing.<Context> - Supervisor modules:
BookProcessing.<Context>.Supervisor - Worker modules:
BookProcessing.<Context>.<Worker> - Utility modules:
BookProcessing.Utilities.<Utility> - Protocol implementations:
BookProcessing.<Context>.<Protocol>.<Type>
4. Interface Patterns
We will use consistent interface patterns across modules:
- Public API: Clear, well-documented public functions
- Private implementation: Internal functions marked as
defp - Behavior definitions: Use
@callbackfor defining interfaces - Protocol implementations: Use protocols for polymorphic behavior
5. State Management
We will use appropriate state management approaches:
- GenServer state: For stateful components
- Immutable data transformation: For data processing pipelines
- ETS tables: For shared, concurrent state when needed
- Agents: For simple state storage
Implementation Examples
Application Module
defmodule BookProcessing do
@moduledoc """
BookProcessing is an ebook conversion system that transforms manuscripts
into various ebook formats with validation and quality control.
"""
@doc """
Converts a manuscript to the specified format.
## Examples
iex> BookProcessing.convert_to_format("manuscript.tex", :pdf)
{:ok, "output/manuscript.pdf"}
"""
def convert_to_format(manuscript_path, format) do
BookProcessing.Conversion.convert(manuscript_path, format)
end
@doc """
Validates an ebook file.
## Examples
iex> BookProcessing.validate("book.epub")
{:ok, %{valid: true, warnings: []}}
"""
def validate(file_path) do
BookProcessing.Validation.validate(file_path)
end
@doc """
Builds a manuscript into multiple formats.
## Examples
iex> BookProcessing.build("manuscript.tex", [:pdf, :epub])
{:ok, %{pdf: "output/manuscript.pdf", epub: "output/manuscript.epub"}}
"""
def build(manuscript_path, formats) do
BookProcessing.Build.Coordinator.build(manuscript_path, formats)
end
end
Context Module
defmodule BookProcessing.Conversion do
@moduledoc """
The Conversion context handles transforming manuscripts between formats.
"""
alias BookProcessing.Conversion.LaTeXConverter
alias BookProcessing.Conversion.DocxConverter
alias BookProcessing.Conversion.PandocConverter
alias BookProcessing.Conversion.FormatRegistry
@doc """
Converts a manuscript to the specified format.
"""
def convert(manuscript_path, format) do
with {:ok, converter} <- FormatRegistry.get_converter(format),
{:ok, result} <- converter.convert(manuscript_path) do
{:ok, result}
else
{:error, reason} -> {:error, reason}
end
end
@doc """
Lists all available conversion formats.
"""
def available_formats do
FormatRegistry.list_formats()
end
end
Worker Module
defmodule BookProcessing.Conversion.LaTeXConverter do
@moduledoc """
Handles conversion of LaTeX documents to various formats.
"""
use GenServer
# Client API
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def convert(manuscript_path) do
GenServer.call(__MODULE__, {:convert, manuscript_path})
end
# Server callbacks
@impl true
def init(opts) do
{:ok, %{config: opts[:config] || default_config()}}
end
@impl true
def handle_call({:convert, manuscript_path}, _from, state) do
case do_convert(manuscript_path, state.config) do
{:ok, result} -> {:reply, {:ok, result}, state}
{:error, reason} -> {:reply, {:error, reason}, state}
end
end
# Private functions
defp do_convert(manuscript_path, config) do
# Implementation details
{:ok, "output/result.pdf"}
end
defp default_config do
%{output_dir: "output", timeout: 30_000}
end
end
Protocol Example
defprotocol BookProcessing.Validation.Validator do
@doc "Validates a file and returns a validation result"
def validate(validator, file_path)
end
defmodule BookProcessing.Validation.EpubValidator do
defstruct [:config]
end
defimpl BookProcessing.Validation.Validator, for: BookProcessing.Validation.EpubValidator do
def validate(validator, file_path) do
# EPUB validation implementation
{:ok, %{valid: true, warnings: []}}
end
end
Consequences
Positive
- Clear boundaries: Each context has well-defined responsibilities
- Consistent structure: Predictable organization makes navigation easier
- Functional approach: Embraces Elixir's functional paradigm
- Testability: Modular design facilitates testing
- Maintainability: Clear organization improves maintainability
- Supervision alignment: Structure supports our supervision strategy
- Domain alignment: Organization reflects our domain model
Negative
- Learning curve: Team needs to adapt to new organization
- Migration complexity: Mapping Ruby classes to Elixir modules requires careful consideration
- Potential over-modularization: Risk of creating too many small modules
- Documentation overhead: Need to document module relationships
- Interface design challenges: Defining clean interfaces between contexts
Mitigation Strategies
- Module guidelines: Create clear guidelines for module creation and organization
- Code examples: Provide examples of well-structured modules
- Refactoring sessions: Regular sessions to review and refine module structure
- Documentation: Thorough documentation of module relationships
- Gradual implementation: Start with core contexts and expand incrementally
References
- "Designing Elixir Systems with OTP" by James Edward Gray II and Bruce Tate
- "Functional Web Development with Elixir, OTP, and Phoenix" by Lance Halvorsen
- "Domain-Driven Design" by Eric Evans
- "Elixir in Action" by Saša Jurić
- Elixir documentation on module organization
- Phoenix contexts as a model for bounded context organization