For some more concrete details, here are some patterns or rules of thumb I've noticed about when to use .ex vs .exs files:
Almost all code goes in an .ex file in my application's 'library' directory tree, e.g. under lib (or maybe web if the application is an old Phoenix webapp). The code can be called (if it defines public functions or macros) elsewhere in the application or from iex. This is the default.
Probably the most important consideration for deciding between .ex and .exs is where (and when) I want to call that code:
- In the application itself – .ex
- From iex(e.g.iex -S mix) –.ex, if the code is also (or probably will be) used by the application;.exsfor 'ad-hoc' scripts, e.g. for a specific bug or other issue
- From the shell, via mix–.exfor custom Mix tasks, or as data inmix.exs(for simple task aliases)
- From the shell, i.e. as a 'regular script' – .exsand run viaelixir some-script.exs.
What may be surprising is that a good bit of code that starts as one of [2], [3], or [4], eventually ends up in [1] anyways.
And a really important thing to keep in mind is that it's EASY to move code from, e.g. an .exs file to an .ex file (or vice versa).
For [2], and for one application in particular, I setup some extra 'scripts mode' Mix environments for use with iex. Basically, they run a basic 'dev' local instance of (parts of) the app, but connect to the production/staging DB (or other backend services). I have some helper functions/macros for these 'command environments' but mostly I just use application code to, e.g. query the production/staging database and fix/cleanup/inspect some data. If I do need to do something that's even a little 'involved' (complicated), I'll usually create an ad-hoc 'issue script'. It's much easier to write code in my editor than in iex.
For ad-hoc scripts for [2], I have a dev/scripts sub-directory-tree in one project and mostly just name ad-hoc scripts based on the corresponding ticket number (from that project's ticket system), ex. 123.exs. Then, in iex, I can easily run that script:
iex> c "dev/scripts/123.exs"
One nice 'workflow' is to just define a single (top-level) module in the script file:
defmodule Ticket123 do
  ...
  def do_some_one_off_thing ...
end
Then, from iex:
iex> c "dev/scripts/123.exs"
[Ticket123]
iex> Ticket123.do_some_one_off_thing ...
Something something
...
For [3], both task aliases in your project's mix.exs and 'fully' custom Mix tasks are really nice.
Some example aliases from one project:
- Run 'all' migrations – this project has regular (Ecto) database migrations AND some migrations that we want to run 'manually' (i.e. using the 'scripts mode' environments mentioned above). We use manual migrations for things like long-running database queries/commands, ex. adding or modifying an index on a big table.
- Deploy to staging/production
- test– this shadows the builtin Mix task to perform some custom local-database setup
- todo– output search results for TODO comments in our (Elixir) code
Some example custom tasks from the same project:
- Mix.Tasks.OneProject.BootstrapServer– some custom { configuration management / automated infrastructure deployment } code
- Mix.Tasks.OneProject.DbSetup– pulls some configuration data from the Elixir app but mostly just passes that to a shell script
- Mix.Tasks.OneProject.ImportTranslations– downloads translations and other internationalization data from a third-party
- Mix.Tasks.OneProject.RollingDeploy– custom deployment that uses something like a 'blue-green' model, but with a load balancer instead of two entirely separate environments/instances
I haven't used entirely 'regular' Elixir script files much at all in any project – to be called via elixir some-script.exs. Probably the biggest reason not to, i.e. to just create a custom Mix task or an 'issue script', is that custom Mix tasks are nicely documented, i.e. via mix help and mix help some-custom-mix-task, and I really like working at a REPL (iex).