BUCKYVERSE

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:

  1. Aligns with our Domain-Driven Design approach (ADR-0011)
  2. Leverages Elixir's functional paradigm effectively
  3. Provides clear boundaries between different system components
  4. Facilitates testing and maintenance
  5. 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:

4. Interface Patterns

We will use consistent interface patterns across modules:

5. State Management

We will use appropriate state management approaches:

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

  1. Clear boundaries: Each context has well-defined responsibilities
  2. Consistent structure: Predictable organization makes navigation easier
  3. Functional approach: Embraces Elixir's functional paradigm
  4. Testability: Modular design facilitates testing
  5. Maintainability: Clear organization improves maintainability
  6. Supervision alignment: Structure supports our supervision strategy
  7. Domain alignment: Organization reflects our domain model

Negative

  1. Learning curve: Team needs to adapt to new organization
  2. Migration complexity: Mapping Ruby classes to Elixir modules requires careful consideration
  3. Potential over-modularization: Risk of creating too many small modules
  4. Documentation overhead: Need to document module relationships
  5. Interface design challenges: Defining clean interfaces between contexts

Mitigation Strategies

  1. Module guidelines: Create clear guidelines for module creation and organization
  2. Code examples: Provide examples of well-structured modules
  3. Refactoring sessions: Regular sessions to review and refine module structure
  4. Documentation: Thorough documentation of module relationships
  5. Gradual implementation: Start with core contexts and expand incrementally

References