ajur58
Coding with LLMs: CONVENTIONS.md for Elixir - do you have your own? What does it contain?
Hey everyone!
For the past weeks I’ve been using Claude Sonnet 3.5 with Aider as a “pair programming” buddy. It works fairly well, but some things have been constantly nagging me.
I’ve noticed that people are using a doc called conventions to fine-tune the code that gets written by the AI.
Inspired by this, I decided to bite the bullet and write one for myself.
To make things easier - and because I really liked PragDave’s Elixir course - I checked out the repo from the course, and told Claude to generate a conventions doc based on that. After a couple of iterations & adjustments I ended up with this version.
I’m curios if someone has their own conventions doc, what it contains, and also happy to hear your feedback.
Thanks!
# Elixir and Phoenix Best Practices
*Based on Dave Thomas' (PragDave) coding philosophy*
## Core Principles
- **Domain-Driven Design**: Organize code around business domains, not technical layers
- **Functional Core, Imperative Shell**: Pure domain logic with side effects at boundaries
- **Explicit Over Implicit**: Prefer clarity over magic
- **Composition Over Inheritance**: Build systems from small, focused components
- **Single Responsibility**: Each module and function should do one thing well
- **Easy to Change**: Design for maintainability and future change
- **Fail Fast**: Detect and handle errors as early as possible
- **YAGNI**: Don't build features until they're needed
## Project Structure
- **Context-Based Organization**: Use Phoenix contexts to define domain boundaries
lib/my_app/
accounts/ # User management domain
billing/ # Payment processing domain
catalog/ # Product catalog domain
- **API/Implementation Separation**: Public API modules delegate to implementation modules
# In MyApp.Accounts (API module)
defdelegate create_user(attrs), to: MyApp.Accounts.UserCreator
- **Boundary Enforcement**: Use tools like NimbleOptions to validate inputs at boundaries
## Coding Patterns
- **Pattern Matching**: Use pattern matching in function heads for control flow
- **Railway-Oriented Programming**: Chain operations with `with` for elegant error handling
with {:ok, user} <- find_user(id),
{:ok, updated} <- update_user(user, attrs) do
{:ok, updated}
end
- **Type Specifications**: Add typespecs to all public functions
@spec create_user(user_attrs()) :: {:ok, User.t()} | {:error, Changeset.t()}
- **Immutable Data Transformations**: Return new state rather than modifying existing state
- **Data Validation**: Validate data at boundaries using Ecto.Changeset even outside of database contexts
def validate_attrs(attrs) do
{%{}, %{name: :string, email: :string}}
|> Ecto.Changeset.cast(attrs, [:name, :email])
|> Ecto.Changeset.validate_required([:name, :email])
|> Ecto.Changeset.validate_format(:email, ~r/@/)
end
- **Result Tuples**: Return tagged tuples like `{:ok, result}` or `{:error, reason}` for operations that can fail
## Process Design
- **GenServer for State**: Use GenServers for stateful processes
- **Supervision Trees**: Design proper supervision hierarchies
- **Registry Pattern**: Use Registry for dynamic process lookup
- **Task.Supervisor**: Use for concurrent, potentially failing operations
- **Process Isolation**: Design processes to crash independently without affecting the whole system
- **Let It Crash**: Embrace the "let it crash" philosophy with proper supervision
## Phoenix Best Practices
- **LiveView-First**: Use LiveView as the primary UI technology
- **Function Components**: Use function components for reusable UI elements
- **PubSub for Real-time**: Use Phoenix PubSub for real-time features
- **Context Boundaries**: Respect context boundaries in controllers and LiveViews
- **Thin Controllers**: Keep controllers thin, delegating business logic to contexts
- **Security First**: Always consider security implications (CSRF, XSS, etc.)
## Testing Strategies
- **Test Public APIs**: Focus on testing public context APIs
- **Mox for Dependencies**: Use Mox for mocking external dependencies
- **Property-Based Testing**: Use StreamData for property-based tests
- **Test Factories**: Use ExMachina for test data creation
- **Test Readability**: Write tests that serve as documentation
- **Arrange-Act-Assert**: Structure tests with clear setup, action, and verification phases
## HTTP and API Integration
- **Req for HTTP Clients**: Use Req instead of HTTPoison or Tesla
- **Behaviours for API Clients**: Define behaviours for API clients to allow easy mocking
- **Error Handling**: Handle network failures and unexpected responses gracefully
- **Timeouts**: Always set appropriate timeouts for external calls
- **Circuit Breakers**: Use circuit breakers for critical external services
## Naming Conventions
- **Snake Case**: For variables and functions (`create_user`)
- **Verb-First Functions**: Start function names with verbs (`create_user`, not `user_create`)
- **Plural for Collections**: Use plural for collections (`users`, not `user`)
- **Consistent Terminology**: Use consistent terms throughout the codebase
- **Intention-Revealing Names**: Choose names that reveal intent, not implementation
## Documentation and Quality
- **Document Public Functions**: Add `@doc` to all public functions
- **Examples in Docs**: Include examples in documentation
- **Credo and Dialyzer**: Use for static analysis and type checking
- **Consistent Formatting**: Use `mix format` to maintain consistent code style
- **Continuous Refactoring**: Regularly improve code structure without changing behavior
- **Comments**: Write comments only when necessary. Describe why, not what it does.
## Performance Considerations
- **Avoid N+1 Queries**: Use Ecto's preloading and joins
- **Pagination**: Paginate large result sets
- **Background Jobs**: Use Oban for background processing
- **Measure First**: Profile before optimizing
- **Caching**: Apply strategic caching where appropriate
Most Liked
ajur58
Here is the file on GitHub for better readability: llm-conventions/conventions_ex.md at main · ajur58/llm-conventions · GitHub
dimitarvp
Same, and always has.
But I understand it’s a noble intention that was hijacked by a lot of wannabes who poisoned the well for the well-intentioned people so I try to suppress my instinctive reaction.
We actually had a friendly disagreement with Renato on this and we both agreed it’s ultimately a matter of taste. At least I maintain that having a plethora of super small files does not improve anything measurably compared to, say, a Phoenix context file (where you have list, get_*, delete etc. helpers).
To me the important thing is the single responsibility principle and clean boundaries. How are these achieved is a technical detail that’s often not very important.
Pal
It was PragDaves approach that led me to Ash ![]()
Popular in Discussions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









