Documentation Practices
Module Documentation with @moduledoc
Use @moduledoc to document modules with comprehensive overviews and usage examples.
defmodule DonationProcessing do
@moduledoc """
Processes charitable donations with Shariah compliance validation.
# => Module for handling donation transactions
# => Ensures Islamic finance principles (no riba, proper zakat)
This module provides functions for:
* Creating donation records
* Validating Shariah compliance
* Processing payment transactions
* Generating tax receipts
## Examples
iex> DonationProcessing.create_donation(%{amount: 1000, currency: "USD"})
# => Creates donation with amount validation
{:ok, %Donation{amount: 1000, currency: "USD", status: :pending}}
# => Returns tuple with :ok atom and Donation struct
iex> DonationProcessing.validate_shariah_compliance(%Donation{})
# => Validates donation meets Islamic finance rules
{:ok, :compliant}
# => Returns :compliant status if validation passes
"""
# Function definitions...
end
# => Complete module documentation visible on hex.pm
# => Examples run as doctests during testing@moduledoc patterns:
- Overview first: Brief description of module purpose
- Feature list: Bullet points for main capabilities
- Examples section: Practical usage demonstrations
- Links to related: Reference connected modules
Function Documentation with @doc
Use @doc to document individual functions with clear signatures and examples.
defmodule DonationProcessing do
@doc """
Creates a new donation record with validation.
# => Function creates donation and validates amount
# => Returns {:ok, donation} or {:error, reason}
## Parameters
* `attrs` - Map with donation attributes
* `:amount` (required) - Donation amount (positive integer)
* `:currency` (required) - ISO 4217 currency code
* `:donor_id` (required) - UUID of donor
* `:project_id` (optional) - Target project UUID
## Returns
* `{:ok, %Donation{}}` - Successfully created donation
* `{:error, changeset}` - Validation failed with errors
## Examples
iex> create_donation(%{amount: 500, currency: "USD", donor_id: "abc123"})
# => Creates donation with minimum required fields
{:ok, %Donation{amount: 500, currency: "USD"}}
# => Returns success tuple with Donation struct
iex> create_donation(%{amount: -100, currency: "USD"})
# => Negative amount fails validation
{:error, %Ecto.Changeset{errors: [amount: {"must be positive", []}]}}
# => Returns error tuple with changeset containing validation errors
"""
@spec create_donation(map()) :: {:ok, Donation.t()} | {:error, Ecto.Changeset.t()}
def create_donation(attrs) do
# => Validates and creates donation record
%Donation{}
|> Donation.changeset(attrs)
# => Applies validation rules from changeset
|> Repo.insert()
# => Persists to database and returns result
end
end@doc patterns:
- Purpose statement: What function does
- Parameters section: Detailed argument descriptions
- Returns section: All possible return values
- Examples: Show common and edge cases
Doctests for Executable Documentation
Doctests run code examples from documentation as automated tests.
defmodule DonationAmount do
@doc """
Converts donation amount to target currency.
# => Currency conversion for international donations
# => Uses current exchange rates
## Examples
iex> DonationAmount.convert(1000, :usd, :idr, 15000.0)
# => Converts 1000 USD to IDR using rate 15000
15_000_000
# => Returns IDR amount (integer, no decimal)
iex> DonationAmount.convert(0, :usd, :idr, 15000.0)
# => Zero amount converts to zero
0
# => Edge case: no conversion needed
iex> DonationAmount.convert(100, :usd, :usd, 1.0)
# => Same currency returns original amount
100
# => No conversion when currencies match
"""
@spec convert(integer(), atom(), atom(), float()) :: integer()
def convert(amount, from_currency, to_currency, rate) do
# => Return original amount if same currency
if from_currency == to_currency do
amount
else
# => Apply exchange rate and round
round(amount * rate)
# => Returns integer amount in target currency
end
end
endRunning doctests:
# test/donation_amount_test.exs
defmodule DonationAmountTest do
use ExUnit.Case, async: true
# => Enables parallel test execution
doctest DonationAmount
# => Runs all doctests from module documentation
# => Each iex> example becomes a test case
# Additional unit tests...
endDoctest patterns:
- Use
iex>prompts: Indicates interactive Elixir code - Show expected output: Next line after prompt
- Add inline comments: Explain with
# =>notation - Test edge cases: Zero values, empty inputs, boundary conditions
ExDoc Generation for hex.pm
ExDoc generates beautiful HTML documentation published to hex.pm.
# mix.exs
defmodule DonationProcessing.MixProject do
use Mix.Project
def project do
[
app: :donation_processing,
version: "0.1.0",
# => Package version for hex.pm
elixir: "~> 1.15",
# => Minimum Elixir version requirement
# Documentation
name: "DonationProcessing",
# => Display name in documentation
source_url: "https://github.com/org/donation_processing",
# => GitHub repository link
homepage_url: "https://oseplatform.com/donation",
# => Project homepage
docs: [
# => ExDoc configuration options
main: "DonationProcessing",
# => Landing page module
logo: "assets/logo.png",
# => Logo displayed in docs
extras: ["README.md", "CHANGELOG.md", "guides/getting-started.md"],
# => Additional markdown files
groups_for_modules: [
# => Organize modules into groups
"Core": [
DonationProcessing,
DonationProcessing.Validator
],
"Payment": [
DonationProcessing.Payment,
DonationProcessing.Receipt
]
],
groups_for_functions: [
# => Group functions by category
"CRUD Operations": &(&1[:section] == :crud),
"Validation": &(&1[:section] == :validation)
]
],
deps: deps()
]
end
defp deps do
[
{:ex_doc, "~> 0.31", only: :dev, runtime: false}
# => ExDoc dependency for documentation generation
# => Only loaded in development, not production
]
end
endGenerating documentation:
# Generate HTML documentation
mix docs
# => Builds documentation in doc/ directory
# => Creates searchable HTML with syntax highlighting
# Open documentation locally
open doc/index.html
# => View generated documentation in browser
# => Test appearance before publishingPublishing to hex.pm:
# Publish package with documentation
mix hex.publish
# => Uploads package and documentation to hex.pm
# => Documentation becomes public at hexdocs.pm/donation_processingDocumentation Best Practices
What to Document
Public functions (always):
@doc """
Public API function - always document with @doc.
# => Users depend on this function
# => Needs clear documentation
"""
def public_function(arg), do: # ...Private functions (selectively):
@doc false
# => Hides from public documentation
# => Still visible in source code
defp complex_internal_logic(data) do
# Implementation with inline comments
# Complex logic explained in comments
endModule overview (always):
@moduledoc """
Every module needs @moduledoc explaining purpose.
# => Appears on hex.pm documentation
# => First thing users read
"""When to Document
Before writing implementation:
# 1. Write documentation first (TDD for docs)
@doc """
Validates Shariah compliance of donation.
# => Write expected behavior first
"""
# 2. Then implement function
def validate_shariah_compliance(donation) do
# Implementation follows documentation
endDocument as you code:
- Add
@moduledocwhen creating module - Write
@docbefore implementing function - Add doctests for examples
- Update docs when behavior changes
Documentation Patterns
Pattern 1: Example-driven documentation:
@doc """
Calculates zakat amount for donation.
## Examples
iex> calculate_zakat(10000)
# => 2.5% of donation amount
250
# => Zakat rate according to Shariah
iex> calculate_zakat(0)
# => No zakat for zero amount
0
"""
def calculate_zakat(amount), do: div(amount * 25, 1000)
# => Simple implementation after clear examplesPattern 2: Parameter validation documentation:
@doc """
Processes donation with validation.
## Parameters
* `donation` - %Donation{} struct (required)
* Must have positive `:amount`
* Must have valid `:currency` (ISO 4217)
* Must have `:donor_id` (UUID format)
## Raises
* `ArgumentError` - If donation invalid
* `Ecto.NoResultsError` - If donor not found
"""
def process_donation(donation) do
# Implementation with validation
endPattern 3: Type-driven documentation:
@typedoc """
Donation struct representing charitable contribution.
# => Custom type with field documentation
## Fields
* `:amount` - Positive integer in smallest currency unit
* `:currency` - Atom representing ISO 4217 code
* `:donor_id` - UUID string identifying donor
* `:status` - One of :pending, :completed, :failed
"""
@type t :: %__MODULE__{
amount: pos_integer(),
# => Amount in cents/fils/smallest unit
currency: atom(),
# => :usd, :eur, :idr, etc.
donor_id: String.t(),
# => UUID v4 format string
status: :pending | :completed | :failed
# => Current donation processing status
}Type Specifications as Documentation
Typespecs document function signatures and enable static analysis.
defmodule DonationAPI do
@typedoc "Donation creation attributes"
@type donation_attrs :: %{
amount: pos_integer(),
# => Positive amount required
currency: String.t(),
# => ISO 4217 currency code
donor_id: String.t(),
# => Donor UUID
project_id: String.t() | nil
# => Optional project assignment
}
@typedoc "Result of donation processing"
@type processing_result ::
{:ok, Donation.t()}
# => Success with donation record
| {:error, :invalid_amount}
# => Amount validation failed
| {:error, :unsupported_currency}
# => Currency not accepted
| {:error, Ecto.Changeset.t()}
# => General validation errors
@doc """
Creates and processes donation transaction.
"""
@spec process_donation(donation_attrs()) :: processing_result()
def process_donation(attrs) do
# => Implementation with type safety
# => Dialyzer can verify correctness
end
endTypespec patterns:
@typefor public types@typepfor private types@specfor function signatures@typedocfor type documentation
Financial Domain Example: Complete Documentation
defmodule DonationReceipt do
@moduledoc """
Generates tax-deductible donation receipts.
# => Module handles receipt generation for donors
# => Complies with tax authority requirements
Receipts include:
* Donation amount and currency
* Donor information
* Organization tax ID
* Receipt date and number
* Tax deductibility statement
## Examples
iex> donation = %Donation{amount: 5000, currency: "USD"}
# => Create sample donation
iex> DonationReceipt.generate(donation)
# => Generate receipt PDF
{:ok, %Receipt{number: "2025-0001", amount: 5000}}
# => Returns receipt with unique number
"""
@typedoc """
Receipt record for donation transaction.
# => Struct contains all receipt information
# => Generated once per successful donation
"""
@type t :: %__MODULE__{
number: String.t(),
# => Unique receipt number (format: YYYY-NNNN)
donation_id: String.t(),
# => Associated donation UUID
amount: pos_integer(),
# => Donation amount in smallest currency unit
currency: String.t(),
# => ISO 4217 currency code
generated_at: DateTime.t(),
# => Receipt generation timestamp (UTC)
pdf_url: String.t() | nil
# => S3 URL for PDF download (nil if not uploaded)
}
defstruct [:number, :donation_id, :amount, :currency, :generated_at, :pdf_url]
@doc """
Generates receipt for completed donation.
# => Creates receipt record and PDF document
# => Uploads to S3 storage
## Parameters
* `donation` - %Donation{} with :completed status
## Returns
* `{:ok, %Receipt{}}` - Receipt generated successfully
* `{:error, :invalid_status}` - Donation not completed
* `{:error, reason}` - PDF generation or upload failed
## Examples
iex> donation = %Donation{id: "abc", amount: 1000, status: :completed}
# => Completed donation ready for receipt
iex> DonationReceipt.generate(donation)
# => Generates receipt with PDF
{:ok, %Receipt{number: "2025-0123", amount: 1000}}
# => Returns receipt with unique number
iex> pending = %Donation{status: :pending}
# => Donation not yet completed
iex> DonationReceipt.generate(pending)
# => Cannot generate receipt for pending donation
{:error, :invalid_status}
# => Returns error tuple
"""
@spec generate(Donation.t()) :: {:ok, t()} | {:error, atom()}
def generate(%Donation{status: :completed} = donation) do
# => Generate receipt number
number = generate_receipt_number()
# => Returns format "YYYY-NNNN"
# => Create receipt record
receipt = %__MODULE__{
number: number,
donation_id: donation.id,
amount: donation.amount,
currency: donation.currency,
generated_at: DateTime.utc_now()
}
# => Receipt struct with donation data
# => Generate PDF document
with {:ok, pdf_binary} <- generate_pdf(receipt),
# => PDF generation from template
{:ok, url} <- upload_to_s3(pdf_binary, number) do
# => Upload to cloud storage
{:ok, %{receipt | pdf_url: url}}
# => Return receipt with PDF URL
end
end
def generate(%Donation{status: status}) do
# => Invalid status handling
{:error, :invalid_status}
# => Returns error for non-completed donations
end
@doc false
# => Private helper hidden from documentation
defp generate_receipt_number do
# => Implementation with inline comments
year = DateTime.utc_now().year
# => Current year for prefix
sequence = get_next_sequence()
# => Database sequence number
"#{year}-#{String.pad_leading("#{sequence}", 4, "0")}"
# => Format: "2025-0001"
end
endSummary
Elixir documentation practices for production systems:
Module documentation:
- Use
@moduledocfor comprehensive module overviews - Include examples, features, and related modules
- Write documentation visible on hex.pm
Function documentation:
- Use
@docfor public API functions - Document parameters, returns, and examples
- Hide private functions with
@doc false
Doctests:
- Write executable examples in documentation
- Test edge cases and common scenarios
- Run with
doctest ModuleNamein tests
ExDoc generation:
- Configure in mix.exs with extras and grouping
- Generate with
mix docsfor local review - Publish to hex.pm with
mix hex.publish
Best practices:
- Document before implementing (documentation-driven)
- Use typespecs for static analysis
- Keep examples realistic and tested
- Update documentation with code changes
Documentation hierarchy: @moduledoc → @doc → @typedoc → inline comments → doctests