Powered by AppSignal & Oban Pro

Elixir Roadmap Completo: Do Basico ao Avancado

elixir/Elixir_Roadmap_Completo.livemd

Elixir Roadmap Completo: Do Basico ao Avancado

> Este Livebook foi desenhado como trilha progressiva de estudo e referencia de arquitetura. > Objetivo: explicar com profundidade o que usar, por que usar, e como validar na pratica.

Como usar este Livebook

  1. Execute as celulas em ordem na primeira passada.
  2. Refaca os exercicios sem olhar as respostas sugeridas.
  3. Adapte os exemplos para um dominio real (faturamento, pedidos, autenticacao, etc).
  4. Mantenha foco em programacao funcional: dados entram, dados saem, efeitos isolados.

Roadmap

  1. Fundamentos de linguagem e pensamento funcional
  2. Ferramentas de produtividade (IEx, Mix, testes)
  3. Scripts robustos e automacao
  4. Banco de dados com Ecto
  5. Concorrencia e OTP
  6. Phoenix API profissional
  7. Observabilidade, seguranca e performance
  8. Cloud e operacao
  9. Elixir avancado (linguagem, runtime e distribuicao)
  10. Especializacoes opcionais (LiveView, umbrella, CRDT, Nerves, internals)

1. Setup e ciclo de desenvolvimento

1.1 Verificacao de ambiente

System.version()

Explicacao:

  1. System.version/0 retorna a versao de Elixir em runtime.
  2. Esta chamada e util para validar compatibilidade de exemplos e bibliotecas.

1.2 Comandos base de projeto

mix new playground
cd playground
mix test
iex -S mix

Motivo de cada comando:

  1. mix new cria estrutura minima de app Elixir.
  2. mix test valida que o projeto inicia com suite verde.
  3. iex -S mix abre shell interativo carregando a aplicacao.

1.3 Exercicio

  1. Crie um projeto roadmap_playground.
  2. Rode mix test e confirme resultado verde.
  3. Abra iex -S mix e execute System.version().

2. Fundamentos da linguagem com explicacao linha a linha

2.1 Imutabilidade e rebind

number = 10
number = number + 5
number

Linha a linha:

  1. number = 10: cria binding para number apontando para o valor 10.
  2. number = number + 5: cria novo binding para number, agora com 15.
  3. number: retorna o valor mais recente associado ao nome.

Motivo do modelo:

  1. Elimina classe inteira de bugs por estado mutavel compartilhado.
  2. Facilita raciocinio em concorrencia.

2.2 Pattern matching estrutural

{:ok, user} = {:ok, %{id: 1, name: "Ana"}}
%{id: id, name: name} = user
{id, name}

Linha a linha:

  1. A tupla {:ok, user} exige formato exato de sucesso.
  2. O map pattern %{id: id, name: name} extrai campos com nomes explicitos.
  3. {id, name} confirma dados extraidos.

Motivo:

  1. Contratos de entrada ficam auto-documentados no proprio codigo.
  2. Fluxos invalidos falham cedo.

2.3 Funcoes pequenas e composicao

defmodule Price do
  def apply_discount(total, percent) when is_number(total) and is_number(percent) do
    total
    |> Kernel.*(1 - percent / 100)
    |> Float.round(2)
  end
end

Price.apply_discount(200.0, 10)

Linha a linha:

  1. defmodule Price: define fronteira de responsabilidade.
  2. Guard when ...: valida precondicoes no contrato da funcao.
  3. Pipe aplica transformacoes em sequencia legivel.
  4. Float.round(2) fixa precisao para representacao de dinheiro no exemplo.

Motivo:

  1. Funcoes pequenas sao testaveis e reutilizaveis.
  2. Pipe favorece leitura declarativa.

2.4 case, cond, with

defmodule Parser do
  def parse_positive_int(text) do
    case Integer.parse(text) do
      {value, ""} when value > 0 -> {:ok, value}
      _ -> {:error, :invalid_positive_int}
    end
  end

  def classify_temp(temp) do
    cond do
      temp < 10 -> :cold
      temp < 25 -> :mild
      true -> :hot
    end
  end

  def parse_and_classify(text) do
    with {:ok, value} <- parse_positive_int(text) do
      {:ok, classify_temp(value)}
    end
  end
end

{Parser.parse_positive_int("12"), Parser.parse_and_classify("30")}

Motivo de cada construcao:

  1. case: melhor quando decisao depende de forma do dado.
  2. cond: melhor para regras booleanas.
  3. with: melhor para pipeline de sucesso/erro com tuplas.

2.5 Exercicios

  1. Crie parse_email/1 que retorna {:ok, email} ou {:error, :invalid_email}.
  2. Crie with que chama parse_email/1 e load_user_by_email/1.
  3. Faca fallback de erro sem excecao como fluxo normal.

3. Colecoes, Enum, Stream e recursao

3.1 Enum (eager) vs Stream (lazy)

values = 1..1_000_000

first_five_evens_times_three =
  values
  |> Stream.map(&amp;(&amp;1 * 3))
  |> Stream.filter(&amp;(rem(&amp;1, 2) == 0))
  |> Enum.take(5)

first_five_evens_times_three

Linha a linha:

  1. values define range grande.
  2. Stream.map nao processa tudo de imediato.
  3. Stream.filter continua lazy.
  4. Enum.take(5) dispara processamento apenas do necessario.

Motivo:

  1. Reduz uso de memoria em volumes altos.
  2. Permite pipelines previsiveis para ETL.

3.2 Recursao com acumulador

defmodule SumList do
  def run(list), do: do_run(list, 0)

  defp do_run([], acc), do: acc
  defp do_run([head | tail], acc), do: do_run(tail, acc + head)
end

SumList.run([1, 2, 3, 4])

Motivo:

  1. Recursao e base da iteracao na BEAM.
  2. Acumulador evita recomputacao e melhora clareza de estado de progresso.

3.3 Exercicios

  1. Implemente product/1 recursivo.
  2. Implemente count_if/2 sem Enum.count/2.
  3. Compare versao recursiva com versao Enum e explique trade-off.

4. Erros como dados e contratos claros

4.1 Tuplas de sucesso/erro

defmodule AuthFlow do
  def sign_in(email, pass) do
    with {:ok, user} <- find_user(email),
         :ok <- validate_password(user, pass),
         {:ok, token} <- issue_token(user) do
      {:ok, token}
    else
      {:error, reason} -> {:error, reason}
    end
  end

  defp find_user("ana@example.com"), do: {:ok, %{id: 1, email: "ana@example.com", hash: "123"}}
  defp find_user(_), do: {:error, :not_found}

  defp validate_password(%{hash: hash}, pass) when hash == pass, do: :ok
  defp validate_password(_, _), do: {:error, :invalid_credentials}

  defp issue_token(user), do: {:ok, "token-#{user.id}"}
end

AuthFlow.sign_in("ana@example.com", "123")

Por que assim:

  1. Fluxo explicito de erro e sucesso.
  2. Nao mistura excecao com controle normal de negocio.
  3. Facilita testes de cenarios negativos.

4.2 Exercicios

  1. Adicione etapa de check_user_status/1 no with.
  2. Retorne {:error, :blocked_user} quando status for "blocked".
  3. Escreva testes para cada ramo.

5. Arquitetura funcional aplicada

5.1 Regra pura separada de IO

defmodule CheckoutDomain do
  def compute_total(items, policy) do
    subtotal = items |> Enum.map(&amp;(&amp;1.price * &amp;1.qty)) |> Enum.sum()
    apply_policy(subtotal, policy)
  end

  defp apply_policy(subtotal, %{type: :percent, value: percent}), do: Float.round(subtotal * (1 - percent / 100), 2)
  defp apply_policy(subtotal, %{type: :flat, value: value}), do: max(subtotal - value, 0)
end

defmodule CheckoutUseCase do
  def execute(items, policy, save_fn) do
    total = CheckoutDomain.compute_total(items, policy)
    save_fn.(%{items: items, total: total})
  end
end

CheckoutUseCase.execute([
  %{price: 100.0, qty: 1},
  %{price: 50.0, qty: 2}
], %{type: :percent, value: 10}, fn order -> {:ok, order} end)

Motivo de design:

  1. Dominio nao depende de banco, HTTP ou fila.
  2. Caso de uso orquestra efeitos por injecao de dependencia funcional (save_fn).

5.2 Exercicios

  1. Adicione cupom :buy_x_get_y no dominio.
  2. Mantenha funcao pura sem acesso a estado global.
  3. Escreva testes unitarios da nova regra.

6. Scripts robustos e automacao

6.1 Script com parametros e validacao

args = ["--from", "2026-01-01", "--to", "2026-01-31", "--format", "json"]

{opts, _rest, _invalid} =
  OptionParser.parse(args,
    switches: [from: :string, to: :string, format: :string],
    aliases: [f: :from, t: :to]
  )

params = %{
  from: Keyword.get(opts, :from),
  to: Keyword.get(opts, :to),
  format: Keyword.get(opts, :format, "json")
}

params

Motivo:

  1. Parametros declarados com tipo e alias.
  2. Defaults controlados explicitamente.
  3. Estrutura final em map facilita serializacao e pipeline.

6.2 Template funcional para script produtivo

defmodule ScriptTemplate do
  def run(input, load_fn, transform_fn, persist_fn) do
    with {:ok, loaded} <- load_fn.(input),
         {:ok, transformed} <- transform_fn.(loaded),
         :ok <- persist_fn.(transformed) do
      :ok
    else
      {:error, reason} -> {:error, reason}
    end
  end
end

load_fn = fn value -> {:ok, value} end
transform_fn = fn value -> {:ok, String.upcase(value)} end
persist_fn = fn _ -> :ok end

ScriptTemplate.run("hello", load_fn, transform_fn, persist_fn)

6.3 Exercicios

  1. Adicione retry funcional ao persist_fn.
  2. Crie modo dry_run sem gravacao.
  3. Retorne metrica de tempo total em milissegundos.

7. Ecto e banco de dados

7.1 Schema e changeset

defmodule UserEntity do
  use Ecto.Schema
  import Ecto.Changeset

  @required_fields [:email, :name]

  schema "users" do
    field :email, :string
    field :name, :string
    field :status, :string, default: "active"
    timestamps(type: :utc_datetime)
  end

  def changeset(struct, attrs) do
    struct
    |> cast(attrs, @required_fields ++ [:status])
    |> validate_required(@required_fields)
    |> validate_format(:email, ~r/@/)
    |> validate_length(:name, min: 2, max: 120)
  end
end

Motivo de cada etapa:

  1. cast limita campos aceitos.
  2. validate_required garante obrigatorios.
  3. validate_format modela contrato sem excecao.
  4. validate_length protege consistencia de dados.

7.2 Contexto como fronteira

defmodule AccountsContext do
  import Ecto.Query, warn: false

  def list_active_users(repo) do
    from(u in UserEntity, where: u.status == "active")
    |> repo.all()
  end

  def create_user(repo, attrs) do
    %UserEntity{}
    |> UserEntity.changeset(attrs)
    |> repo.insert()
  end
end

Motivo:

  1. Controller/chamada externa nao conhece query detalhada.
  2. Regras de persistencia ficam encapsuladas em API interna clara.

7.3 Exercicio

  1. Adicione deactivate_user/2 com update seguro.
  2. Garanta retorno {:ok, user} ou {:error, changeset}.
  3. Escreva teste de integracao para os dois fluxos.

8. Concorrencia e OTP

8.1 Processos e mailbox

parent = self()

spawn(fn ->
  send(parent, {:result, 42})
end)

receive do
  {:result, value} -> {:ok, value}
after
  1_000 -> {:error, :timeout}
end

Motivo:

  1. Processo leve e isolado.
  2. Mensagem explicita entre fronteiras de concorrencia.
  3. Timeout evita bloqueio infinito.

8.2 Task com limite de concorrencia

1..20
|> Task.async_stream(fn n -> n * 10 end, max_concurrency: 5, timeout: 3_000)
|> Enum.map(fn {:ok, value} -> value end)
|> Enum.take(5)

Motivo:

  1. Controle de throughput com max_concurrency.
  2. Evita saturar CPU/IO de forma descontrolada.

8.3 GenServer para estado coordenado

defmodule CounterServer do
  use GenServer

  def start_link(initial), do: GenServer.start_link(__MODULE__, initial, name: __MODULE__)
  def increment, do: GenServer.call(__MODULE__, :increment)
  def current, do: GenServer.call(__MODULE__, :current)

  @impl true
  def init(initial), do: {:ok, initial}

  @impl true
  def handle_call(:increment, _from, state), do: {:reply, state + 1, state + 1}

  @impl true
  def handle_call(:current, _from, state), do: {:reply, state, state}
end

8.4 Supervisor

children = [
  {CounterServer, 0}
]

Supervisor.start_link(children, strategy: :one_for_one)

8.5 Exercicios

  1. Crie WalletServer com credit/1, debit/1, balance/0.
  2. Isole regra de saldo em modulo puro (WalletLogic).
  3. Adicione supervisor com estrategia apropriada.

9. Phoenix API profissional

9.1 Controller fino e contexto

defmodule PostControllerExample do
  def create(params, create_post_fn) do
    with {:ok, post} <- create_post_fn.(params) do
      {:created, post}
    else
      {:error, reason} -> {:unprocessable_entity, reason}
    end
  end
end

PostControllerExample.create(%{"title" => "Novo"}, fn attrs -> {:ok, attrs} end)

Motivo:

  1. Controller orquestra HTTP/retorno.
  2. Regra de negocio vive no contexto.
  3. Erros padronizados por contrato de tupla.

9.2 Fallback padrao

defmodule FallbackExample do
  def call({:error, :not_found}), do: {:not_found, %{error: "not_found"}}
  def call({:error, :validation}), do: {:unprocessable_entity, %{error: "validation"}}
end

FallbackExample.call({:error, :not_found})

9.3 Exercicio

  1. Padronize erros :unauthorized, :forbidden, :conflict.
  2. Garanta corpo JSON consistente para todos.
  3. Cubra com testes de contrato.

10. Observabilidade, seguranca e performance

10.1 Telemetria

:telemetry.attach(
  "roadmap-query-handler",
  [:my_app, :repo, :query],
  fn _event, measurements, metadata, _config ->
    %{duration: measurements.total_time, query: metadata.query}
  end,
  nil
)

Motivo:

  1. Instrumenta sem acoplar dominio a logging.
  2. Permite metricas e alertas orientados a evento.

10.2 Seguranca por contrato

defmodule PayloadGuard do
  def validate_size(payload, max_size) when byte_size(payload) <= max_size, do: :ok
  def validate_size(_, _), do: {:error, :payload_too_large}
end

PayloadGuard.validate_size("abc", 5)

10.3 Performance orientada a medicao

defmodule PerfSample do
  def map_enum(values), do: Enum.map(values, &amp;(&amp;1 * 2))
  def map_for(values), do: for n <- values, do: n * 2
end

values = Enum.to_list(1..50_000)

{t1, _} = :timer.tc(fn -> PerfSample.map_enum(values) end)
{t2, _} = :timer.tc(fn -> PerfSample.map_for(values) end)

%{enum_us: t1, for_us: t2}

Exercicio:

  1. Substitua Enum por Stream em volume grande.
  2. Meca novamente e compare latencia e memoria.
  3. Documente o criterio da decisao.

11. AWS e operacao

11.1 Config por ambiente

config = %{
  access_key_id: System.get_env("AWS_ACCESS_KEY_ID"),
  secret_access_key: System.get_env("AWS_SECRET_ACCESS_KEY"),
  region: System.get_env("AWS_REGION", "us-east-1")
}

config

Motivo:

  1. Evita segredo hardcoded.
  2. Permite deploy com mesmo artefato em ambientes distintos.

11.2 Script funcional para envio de evento

defmodule EventPublisher do
  def publish(payload, encode_fn, send_fn) do
    with {:ok, encoded} <- encode_fn.(payload),
         :ok <- send_fn.(encoded) do
      :ok
    else
      {:error, reason} -> {:error, reason}
    end
  end
end

encode_fn = fn payload -> {:ok, :erlang.term_to_binary(payload)} end
send_fn = fn _ -> :ok end

EventPublisher.publish(%{event: "invoice.created"}, encode_fn, send_fn)

Exercicio:

  1. Modele retry com backoff.
  2. Adicione idempotency key por evento.
  3. Registre telemetria por tentativa.

12. Elixir avancado

12.1 Macros com criterio

defmodule EnsureMacro do
  defmacro ensure(expression) do
    quote do
      case unquote(expression) do
        {:ok, value} -> value
        {:error, reason} -> raise "ensure_failed=#{inspect(reason)}"
      end
    end
  end
end

defmodule MacroDemo do
  import EnsureMacro

  def run do
    ensure({:ok, 10})
  end
end

MacroDemo.run()

Motivo:

  1. Macro para reduzir boilerplate repetitivo de compilacao.
  2. Nao usar macro para esconder regra complexa de negocio.

12.2 Protocols e behaviours

defprotocol Exportable do
  def export(data)
end

defimpl Exportable, for: Map do
  def export(data), do: {:ok, data}
end

defmodule ExportAdapter do
  @callback deliver(term()) :: :ok | {:error, term()}
end

12.3 Typespecs e dialyzer

defmodule MoneyMath do
  @type amount :: non_neg_integer()

  @spec add(amount(), amount()) :: amount()
  def add(a, b), do: a + b
end

MoneyMath.add(10, 20)

12.4 Bitstrings

defmodule PacketDecoder do
  def decode(<>), do: {:ok, %{version: version, payload: payload}}
  def decode(_), do: {:error, :invalid_packet}
end

PacketDecoder.decode(<<1, 0, 3, "abc"::binary>>)

12.5 Exercicios

  1. Crie protocol Renderable para Map e List.
  2. Crie behaviour Notifier com adapters Console e Null.
  3. Decodifique pacote com campos flags e checksum em bitstring.

13. Runtime, distribuicao e producao

13.1 ETS e DETS

table = :ets.new(:sessions, [:set, :public, :named_table, read_concurrency: true])
true = :ets.insert(table, {"token-1", %{user_id: 1}})
:ets.lookup(table, "token-1")
{:ok, dets_table} = :dets.open_file(:sessions_disk, [type: :set, file: 'sessions_disk.dets'])
:ok = :dets.insert(dets_table, {"token-1", %{user_id: 1}})
:dets.lookup(dets_table, "token-1")
:dets.close(dets_table)

Motivo:

  1. :ets para leitura/escrita muito rapida em memoria local.
  2. :dets para persistencia local simples.

13.2 :gen_statem para estados explicitos

defmodule DoorFSM do
  @behaviour :gen_statem

  def start_link, do: :gen_statem.start_link({:local, __MODULE__}, __MODULE__, :locked, [])
  def callback_mode, do: :state_functions
  def init(state), do: {:ok, state, %{}}

  def locked(:cast, :unlock, data), do: {:next_state, :unlocked, data}
  def locked(_, _, data), do: {:keep_state, data}

  def unlocked(:cast, :lock, data), do: {:next_state, :locked, data}
  def unlocked(_, _, data), do: {:keep_state, data}
end

13.3 Distribuicao BEAM

Node.self()
Node.list()

Regras de desenho:

  1. Timeout em toda chamada remota.
  2. Operacoes idempotentes para retry.
  3. Plano de reconciliacao apos netsplit.

13.4 Exercicio

  1. Modele agregador distribuido com :rpc.call/4.
  2. Trate timeout e fallback por nodo.
  3. Emita evento de telemetria por falha de nodo.

14. Especializacoes opcionais

14.1 LiveView

defmodule CounterLiveMini do
  use Phoenix.LiveView

  def mount(_params, _session, socket), do: {:ok, assign(socket, :count, 0)}
  def handle_event("inc", _params, socket), do: {:noreply, update(socket, :count, &amp;(&amp;1 + 1))}
end

14.2 Umbrella

mix new platform --umbrella

14.3 CRDT/Consenso

  1. CRDT para convergencia eventual em particao.
  2. Consenso forte apenas para invariantes criticas.

14.4 Nerves

mix nerves.new sensor_hub

14.5 Internals BEAM

  1. AST com quote/unquote.
  2. Chunks BEAM com :beam_lib.
  3. Custo de abstracao medido com benchmark/profiler.

15. Plano de exercicios por nivel

Nivel 1 (Fundamentos)

  1. Parser de parametros com case e with.
  2. Calculadora funcional de desconto com testes.
  3. Recursao para sum, product, count_if.

Nivel 2 (Persistencia)

  1. Contexto Accounts com create/list/get.
  2. Ecto.Multi para fluxo de fatura + pagamento.
  3. Backfill seguro com lotes e idempotencia.

Nivel 3 (API e OTP)

  1. API de posts com fallback padrao.
  2. GenServer + Supervisor para contador distribuido por key.
  3. Telemetria de latencia por endpoint.

Nivel 4 (Producao)

  1. Pipeline assincrono com back-pressure.
  2. Cache com :ets e invalidacao por evento.
  3. Simulacao de falha de nodo com estrategia de recuperacao.

Nivel 5 (Avancado)

  1. Protocol + behaviour para exportacao multi-formato.
  2. Bitstring parser de protocolo binario com checksum.
  3. Macro pequena para reduzir boilerplate seguro.

16. Checklist de maturidade tecnica

  1. Separa dominio puro de fronteiras de IO.
  2. Modela erros como dados ({:error, reason}).
  3. Tem suite de testes com casos de borda e falha.
  4. Usa observabilidade para decidir otimizacao.
  5. Consegue operar release com rollback e runbook.
  6. Entende quando usar GenServer, :gen_statem, :ets, :dets.
  7. Consegue explicar cada escolha tecnica em termos de trade-off.

17. Proximos passos recomendados

  1. Criar um projeto guia real (billing platform) usando todas as camadas.
  2. Medir baseline de latencia e throughput antes de otimizar.
  3. Documentar ADRs (Architecture Decision Records) para escolhas principais.
  4. Rodar revisoes tecnicas quinzenais com foco em clareza de fronteiras.