What’s your 80/20 technique for maintaining a healthy Elixir codebase?

Hey, Elixir novice here coming from Typescript, Go, Ocaml.

What are your favorite techniques to get the most of your compile time checks, code readability and maintainability, and even testing strategies?

I just read this https://ulisses.dev/elixir/2020/02/19/elixir-style-for-maintanability.html, and I actually hadn’t thought to do more pattern matching on structs. Noteworthy to me because the code generated for Ecto’s CRUD functions often just pass along a generic map as the sole argument for parameters. Looking back at my project now, it would’ve served me better if I had used the specific struct as the argument.


I just posted a blog post on how I scope my use of typespecs, trying to find a a type safe balance while enjoying the dynamic nature of Elixir:

I also am really starting to fall into a groove with pattern matching and use it more and more – specifically I like making sure the variables I’m binding values into are the only ones required for a test or function. Trying not to make too many assumptions about the data (so it can hopefully evovle without breaking things).

Other final note, is I’m being much more mindful of how entities cross context boundaries. Still finding my way with LARGE code bases, but I feel like I’m starting to ask the right questions.


Elixir has guard clauses, that I like to use heavily in conjunction with pattern matching:

defp _todo_hash(%{
          title: title, # pattern matching
          user_uid: user_uid,
          date: date,
        } = _attrs,
    when is_binary(title) # guard clauses
    and  byte_size(title) > 0
    and  is_binary(user_uid)
    and  byte_size(user_uid) === 64
    and  is_binary(date)
    and  byte_size(date) === 10
  # your code here

Now that I discovered the Domo library I am using it to have typed structs and then pattern match on them:

defmodule Tasks.Todos.Types.Event do

  # @link https://hexdocs.pm/domo/Domo.html
  use Domo

  typedstruct do
    field :type, :todo | :backlog
    field :target, :todo | :backlog | :all
    field :action, :add | :update | :move | :duplicate | :delete
    field :origin, atom()
    field :broadcast_topics, list(), default: []
    field :context, map(), default: %{}


that are then used like this:

def broadcast_change(
  {:ok, data} = result, 
  %Tasks.Todos.Types.Event{} = event
) do
  # your code here

my number one rule is to just go back tomorrow and read your code. Is there any doubt in your mind that you’ll enjoy reading this code in 2 years? Or worse yet - is there a remote possibility that this code is clear only because of memory?

Other than that this article has a few nice points: http://blog.cretaria.com/posts/erlang-beauty.html

my favs are:

  • keep functions the size of 7 lines
  • turn case statements into functions
  • rename stuff after you’re done with the code

I love short functions and I strive for them, but I am curious why 7 lines and not 10 or 5 or 20?

Thus that 7 lines include comments and blank lines?

Maybe something to do with this: The Magical Number Seven, Plus or Minus Two - Wikipedia

I would imagine they mean 7 operations/assignments and for example a list that is split over n lines (for formatting sake) would just count as 1. Also comments and line breaks would not count, those are just there for context/formatting.

If your function is doing more than 7 things/operations inside of it, it becomes harder to keep all that in your head at once, and thus more prone to bugs…