Cleanup
Torey Heinz
committed Feb 23, 2026
commit 02ac3b97783763a287c2de8499cba41d2449c53c
Showing 3
changed files with
0 additions
and 1179 deletions
01-intro.livemd
+0
-252
| @@ | @@ -1,252 +0,0 @@ |
| - | # Elixir in the Real World |
| - | |
| - | ## A Practical Introduction |
| - | |
| - | **Torey Heinz** |
| - | GR Web Dev Meetup — February 23, 2026 |
| - | |
| - | ## About Me |
| - | |
| - | **Torey Heinz** |
| - | Full-stack developer — 15+ years building web applications |
| - | |
| - | **Vianet Management** — [vianetmanagement.com](https://www.vianetmanagement.com/) |
| - | Huge thanks to Vianet for fully embracing Elixir and giving us the freedom |
| - | to adopt it across our stack. Everything you'll see today runs in production |
| - | because they were willing to invest in a better way to build software. |
| - | |
| - | **Shopflow** — Founder |
| - | Software for small manufacturing businesses. |
| - | |
| - | **My path to Elixir:** |
| - | 15+ years of Ruby on Rails → caught the Elixir bug → haven't looked back. |
| - | |
| - | ## What We're Covering Today |
| - | |
| - | Three parts, about 50 minutes: |
| - | |
| - | 1. **Why Elixir?** — Where it comes from and why it matters |
| - | 2. **The Language** — Pattern matching, processes, and LiveView (live demos!) |
| - | 3. **Real-World Code** — Production systems from my work at Vianet |
| - | |
| - | Then we'll open it up for Q&A. |
| - | |
| - | ## Part 1: Why Elixir? |
| - | |
| - | Before we look at any code, let's answer the question: |
| - | **why does this language exist, and why should you care?** |
| - | |
| - | ## The Origin Story |
| - | |
| - | ### Erlang (1986) |
| - | |
| - | Built by **Ericsson** for telecom switches — systems that literally **could not go down**. |
| - | |
| - | Think about what a phone network requires: |
| - | - Millions of simultaneous calls |
| - | - A single dropped call can't crash the entire system |
| - | - You need to update the software **without** hanging up on everyone |
| - | - 99.9999999% uptime ("nine nines" — about 31 milliseconds of downtime per year) |
| - | |
| - | Erlang was designed from the ground up for: |
| - | - **Concurrency** — handle millions of independent tasks simultaneously |
| - | - **Fault tolerance** — isolated failures, automatic recovery |
| - | - **Distribution** — spread across multiple machines seamlessly |
| - | - **Hot code upgrades** — deploy new code without stopping the system |
| - | |
| - | It's been battle-tested for **40 years** in phone networks, banking systems, |
| - | and messaging infrastructure. |
| - | |
| - | ### The BEAM |
| - | |
| - | The BEAM is Erlang's **virtual machine** — the runtime that actually executes the code. |
| - | |
| - | It's not like the JVM or the Node.js runtime. It was designed from scratch |
| - | specifically for: |
| - | |
| - | - Running **millions of lightweight processes** (not OS threads — much cheaper) |
| - | - **Preemptive scheduling** — no single process can hog the CPU |
| - | - **Per-process garbage collection** — no stop-the-world pauses |
| - | - **Built-in distribution** — processes can communicate across machines |
| - | |
| - | The BEAM is arguably the best runtime ever built for concurrent, networked applications. |
| - | It just had a developer experience problem... |
| - | |
| - | ### Elixir (2012) |
| - | |
| - | **José Valim** — a member of the Ruby on Rails core team — asked: |
| - | |
| - | > *"What if we took the best runtime in the world and made it a joy to write?"* |
| - | |
| - | Elixir brings: |
| - | - **Modern syntax** — inspired by Ruby, approachable for web developers |
| - | - **Excellent tooling** — Mix (build tool), Hex (package manager), ExUnit (testing) |
| - | - **Metaprogramming** — macros that extend the language cleanly |
| - | - **Full access** to the entire Erlang ecosystem — 40 years of battle-tested libraries |
| - | |
| - | Elixir doesn't replace Erlang. It **runs on** Erlang's BEAM. |
| - | You get all of Erlang's power with none of the 1980s syntax. |
| - | |
| - | ## Who's Using Elixir? |
| - | |
| - | This isn't a hobby language. Companies choose Elixir when **reliability and scale |
| - | actually matter:** |
| - | |
| - | **Discord** |
| - | - 5+ million concurrent users |
| - | - Real-time messaging at massive scale |
| - | - Elixir handles their real-time communication infrastructure |
| - | |
| - | **Pinterest** |
| - | - Notification delivery system |
| - | - Billions of events processed |
| - | |
| - | **Pepsi** |
| - | - Real-time manufacturing and logistics systems |
| - | - Monitoring production lines across facilities |
| - | |
| - | **The famous Phoenix demo:** |
| - | > **2 million simultaneous WebSocket connections on a single server.** |
| - | |
| - | That's not a theoretical benchmark. It's a practical demonstration of what |
| - | the BEAM makes possible. |
| - | |
| - | And closer to home: **Vianet Management**, right here in West Michigan, |
| - | running Elixir in production every day. |
| - | |
| - | ## Why It Matters for Web Apps |
| - | |
| - | Here's the key insight that made Elixir click for me: |
| - | |
| - | ### Web applications are fundamentally I/O bound |
| - | |
| - | Think about what your web app actually does when it handles a request: |
| - | |
| - | ``` |
| - | Request arrives |
| - | │ |
| - | ▼ |
| - | ┌─────────┐ |
| - | │ Parse │ ← microseconds |
| - | └────┬─────┘ |
| - | ▼ |
| - | ╔═══════════╗ |
| - | ║ Query DB ║ ← WAITING (milliseconds to seconds) |
| - | ╚═════╤═════╝ |
| - | ▼ |
| - | ┌──────────┐ |
| - | │ Process │ ← microseconds |
| - | └────┬─────┘ |
| - | ▼ |
| - | ╔═══════════╗ |
| - | ║ Call API ║ ← WAITING (milliseconds to seconds) |
| - | ╚═════╤═════╝ |
| - | ▼ |
| - | ┌──────────┐ |
| - | │ Render │ ← microseconds |
| - | └────┬─────┘ |
| - | ▼ |
| - | Response sent |
| - | ``` |
| - | |
| - | Your app spends most of its time **waiting** — on databases, APIs, file systems, |
| - | user input. The actual computation is a tiny fraction. |
| - | |
| - | ### How different languages handle this |
| - | |
| - | **Node.js** — Single-threaded event loop |
| - | - Great for I/O, but one long computation blocks everything |
| - | - Callback complexity (async/await helps, but it's bolted on) |
| - | - No true parallelism for CPU work |
| - | |
| - | **Ruby/Python** — Thread-based with a GIL |
| - | - Global Interpreter Lock limits true concurrency |
| - | - Threads are expensive (~1MB each) |
| - | - You compensate with multiple processes (Puma workers, Gunicorn) |
| - | |
| - | **Java/Go** — Better concurrency primitives |
| - | - Goroutines and virtual threads are lightweight |
| - | - But no built-in fault tolerance or supervision |
| - | |
| - | **Elixir/BEAM** — Purpose-built for this exact problem |
| - | - Lightweight processes (~2KB each, millions possible) |
| - | - Preemptive scheduling (nothing can hog the CPU) |
| - | - Built-in fault tolerance (supervisors restart crashed processes) |
| - | - All I/O is non-blocking by default — no async/await needed |
| - | |
| - | ### Real impact at Vianet |
| - | |
| - | We migrated services from **Ruby on Rails on AWS** to **Elixir on Render**. |
| - | |
| - | The results: |
| - | - **Faster response times** — BEAM processes handle concurrent requests naturally |
| - | - **Fewer resources needed** — one Elixir instance does the work of several Rails workers |
| - | - **~$10,000/month infrastructure savings** — not a typo |
| - | |
| - | This isn't because Rails is bad — I built my career on Rails and I still respect it deeply. |
| - | It's because **Elixir's concurrency model is a natural fit for what web apps actually do.** |
| - | |
| - | When most of your time is spent waiting on I/O, a runtime designed for millions |
| - | of concurrent waiting processes is going to win. |
| - | |
| - | ## A Quick Taste |
| - | |
| - | Before we dive into the language features, here's what Elixir looks like. |
| - | If you've written Ruby or JavaScript, this should feel familiar: |
| - | |
| - | ```elixir |
| - | # Variables, strings, basic data types |
| - | name = "GR Web Dev" |
| - | year = 2026 |
| - | attendees = ["Alice", "Bob", "Charlie"] |
| - | |
| - | IO.puts("Welcome to #{name}, #{year}!") |
| - | IO.puts("We have #{length(attendees)} people here tonight") |
| - | ``` |
| - | |
| - | ```elixir |
| - | # Lists, maps, and the pipe operator |
| - | languages = ["JavaScript", "Ruby", "Python", "Elixir", "Go"] |
| - | |
| - | # The pipe operator |> chains functions together (like Unix pipes) |
| - | languages |
| - | |> Enum.filter(fn lang -> String.length(lang) > 4 end) |
| - | |> Enum.sort() |
| - | |> Enum.join(", ") |
| - | ``` |
| - | |
| - | ```elixir |
| - | # Maps (like JavaScript objects or Ruby hashes) |
| - | talk = %{ |
| - | title: "Elixir in the Real World", |
| - | speaker: "Torey Heinz", |
| - | meetup: "GR Web Dev", |
| - | duration_minutes: 50 |
| - | } |
| - | |
| - | IO.puts("#{talk.speaker} is presenting \"#{talk.title}\" at #{talk.meetup}") |
| - | ``` |
| - | |
| - | ```elixir |
| - | # A simple function |
| - | defmodule Math do |
| - | def factorial(0), do: 1 |
| - | def factorial(n) when n > 0, do: n * factorial(n - 1) |
| - | end |
| - | |
| - | Math.factorial(10) |
| - | ``` |
| - | |
| - | That last example is already using **pattern matching** and **guard clauses** — |
| - | two of the features we'll explore in depth next. |
| - | |
| - | The `|>` pipe operator is worth noting — it's one of those small things that |
| - | makes Elixir code incredibly readable. Data flows left to right, top to bottom, |
| - | just like you'd read it. |
| - | |
| - | ## Let's Dive In |
| - | |
| - | Now that you know **why** Elixir exists and **who** is using it, |
| - | let's look at **what makes it different**. |
| - | |
| - | Next up: **The Language** — pattern matching, processes, and LiveView. |
03-real-world.livemd
+0
-719
| @@ | @@ -1,719 +0,0 @@ |
| - | # Part 3: Real-World Code |
| - | |
| - | ## From Concepts to Production |
| - | |
| - | Everything we just covered — pattern matching, processes, supervision, LiveView — |
| - | these aren't academic concepts. They're the foundation of production systems |
| - | running real businesses **right now**. |
| - | |
| - | Let's walk through three real systems built at Vianet Management. |
| - | |
| - | ## 3.1 — Email Marketing: 500k+ Personalized Emails |
| - | |
| - | ### The Challenge |
| - | |
| - | We needed to send **500,000+ personalized marketing emails** through AWS SES |
| - | (Simple Email Service). |
| - | |
| - | The catch: **if you exceed your rate limit, AWS doesn't tell you — they silently |
| - | drop your emails.** No error, no bounce, no notification. Your email just vanishes. |
| - | |
| - | So we need: |
| - | - A rate limiter that **never** exceeds the AWS SES sending rate |
| - | - The ability to schedule and personalize hundreds of thousands of emails |
| - | - Handling bounces, complaints, and delivery notifications |
| - | - All of it running concurrently without blocking the web app |
| - | |
| - | ### The Architecture |
| - | |
| - | ``` |
| - | ┌─────────────────────────────────────────────────┐ |
| - | │ Application Supervisor │ |
| - | │ │ |
| - | │ ┌──────────┐ ┌──────────────┐ ┌───────────┐ │ |
| - | │ │ Phoenix │ │ EmailRate │ │ Oban │ │ |
| - | │ │ Endpoint │ │ Limiter │ │ (Job Queue│) │ |
| - | │ │ │ │ (GenServer) │ │ │ │ |
| - | │ └──────────┘ └──────────────┘ └───────────┘ │ |
| - | │ ▲ │ │ |
| - | │ │ │ │ |
| - | │ │ ┌────┴────┐ │ |
| - | │ │ │Scheduler│ │ |
| - | │ │ │ Worker │ │ |
| - | │ │ └────┬────┘ │ |
| - | │ │ │ │ |
| - | │ │ ┌─────────┴────────┐ │ |
| - | │ │ │ 2,000 at a time │ │ |
| - | │ │ └─────────┬────────┘ │ |
| - | │ │ │ │ |
| - | │ │ ┌─────────┴────────┐ │ |
| - | │ └─────│ Mailer Workers │ │ |
| - | │ │ (one per email) │ │ |
| - | │ └──────────────────┘ │ |
| - | └─────────────────────────────────────────────────┘ |
| - | ``` |
| - | |
| - | 1. A **Scheduler Worker** queries the database and creates individual email jobs — 2,000 at a time |
| - | 2. Each **Mailer Worker** handles one email: validate, personalize, rate-check, send |
| - | 3. The **Rate Limiter** (a GenServer) ensures we never exceed the AWS SES limit |
| - | |
| - | ### The Supervision Tree |
| - | |
| - | This is the actual `application.ex` — the entry point that starts every process: |
| - | |
| - | <!-- livebook:{"force_markdown":true} --> |
| - | |
| - | ```elixir |
| - | defmodule Marketing.Application do |
| - | use Application |
| - | |
| - | def start(_type, _args) do |
| - | children = [ |
| - | MarketingWeb.Telemetry, |
| - | Marketing.Repo, # Database connection pool |
| - | Marketing.PuppiesRepo, # Multi-database support |
| - | Marketing.ReputableRoomsRepo, |
| - | Marketing.RoommatesRepo, |
| - | Marketing.NuzzleRepo, |
| - | {Phoenix.PubSub, name: Marketing.PubSub}, |
| - | {Finch, name: Marketing.Finch}, # HTTP client |
| - | MarketingWeb.Endpoint, # Web server |
| - | Marketing.EmailRateLimiter, # ← Our rate limiter process |
| - | {Oban, Application.fetch_env!(:marketing, Oban)} # Job queue |
| - | ] |
| - | |
| - | opts = [strategy: :one_for_one, name: Marketing.Supervisor] |
| - | Supervisor.start_link(children, opts) |
| - | end |
| - | end |
| - | ``` |
| - | |
| - | Every process in this list is **supervised**. If the rate limiter crashes, |
| - | the supervisor restarts it. If the web server crashes, same thing. |
| - | The application keeps running. |
| - | |
| - | In **Rails**, you'd need separate processes: Puma, Sidekiq, Redis, maybe a |
| - | custom daemon for rate limiting. In **Elixir**, it's one unified supervision tree. |
| - | |
| - | ### The Rate Limiter — A GenServer in Production |
| - | |
| - | This is the real rate limiter. It uses a **token bucket algorithm** to enforce |
| - | the AWS SES sending rate: |
| - | |
| - | <!-- livebook:{"force_markdown":true} --> |
| - | |
| - | ```elixir |
| - | defmodule Marketing.EmailRateLimiter do |
| - | use GenServer |
| - | |
| - | defstruct tokens: 0.0, |
| - | capacity: 50.0, |
| - | rate: 25.0, |
| - | daily_remaining: 500_000, |
| - | last_refill_ms: nil |
| - | |
| - | # Public API — other code just calls this |
| - | def ready?, do: GenServer.call(__MODULE__, :acquire) |
| - | |
| - | def init(_opts) do |
| - | state = %__MODULE__{last_refill_ms: now_ms()} |
| - | # Self-schedule periodic refills |
| - | Process.send_after(self(), :refill, 1_000) |
| - | {:ok, state} |
| - | end |
| - | |
| - | # Synchronous check: can we send right now? |
| - | def handle_call(:acquire, _from, state) do |
| - | state = refill(state) |
| - | |
| - | cond do |
| - | state.daily_remaining <= 1_000 -> |
| - | {:reply, {:not_ready, :exceeded_daily}, state} |
| - | |
| - | state.tokens >= 1.0 -> |
| - | {:reply, {:ok, :token}, |
| - | %{state | tokens: state.tokens - 1.0, |
| - | daily_remaining: state.daily_remaining - 1}} |
| - | |
| - | true -> |
| - | {:reply, {:not_ready, :exceeded_rate}, state} |
| - | end |
| - | end |
| - | |
| - | # Periodic refill — tokens regenerate over time |
| - | def handle_info(:refill, state) do |
| - | Process.send_after(self(), :refill, 1_000) |
| - | {:noreply, refill(state)} |
| - | end |
| - | end |
| - | ``` |
| - | |
| - | **What's happening here:** |
| - | |
| - | - The GenServer holds the **current token count** and **daily quota** as state |
| - | - Every second, it refills tokens based on the allowed send rate |
| - | - When a worker asks `ready?`, it checks: are there tokens? Is the daily quota OK? |
| - | - If not ready, the worker **snoozes** and retries later — no email is silently dropped |
| - | - All of this is **thread-safe by design** — GenServer processes messages one at a time |
| - | |
| - | **Compare to the alternatives:** |
| - | |
| - | | Approach | Problem | |
| - | |----------|---------| |
| - | | Redis counter (Rails) | Network round-trip for every email, eventual consistency | |
| - | | In-memory variable (Node.js) | Race conditions under concurrent access | |
| - | | Database counter | Way too slow at 500k emails | |
| - | | **GenServer (Elixir)** | **In-process, atomic, zero network overhead** | |
| - | |
| - | ### The Mailer Worker — Pattern Matching in Action |
| - | |
| - | Each email goes through a pipeline. Watch how `with` chains the steps and |
| - | pattern matching handles every possible failure: |
| - | |
| - | <!-- livebook:{"force_markdown":true} --> |
| - | |
| - | ```elixir |
| - | defmodule Marketing.CampaignMailerWorker do |
| - | use Oban.Worker, queue: :campaigns |
| - | |
| - | def perform(%Oban.Job{args: args} = job) do |
| - | campaign_id = Map.get(args, "campaign_id") |
| - | member_id = Map.get(args, "member_id") |
| - | live_mode = Map.get(args, "live_mode") |
| - | |
| - | with {:ok, member} <- get_member_with_site(member_id), |
| - | {:ok, _} <- validate_email(member.email), |
| - | {:ok, campaign} <- get_campaign_with_site(campaign_id), |
| - | {:ok, campaign_email} <- reserve_campaign_email(campaign, member), |
| - | {:ok} <- ensure_campaign_email_not_sent(campaign_email), |
| - | {:ok, _} <- EmailRateLimiter.ready?() do |
| - | send_campaign_email(campaign, member, live_mode) |
| - | else |
| - | {:not_ready, :exceeded_rate} -> {:snooze, 1} |
| - | {:not_ready, :exceeded_daily} -> {:snooze, min(backoff(job), 3_600)} |
| - | {:already_sent} -> {:discard, :already_sent} |
| - | {:invalid_email, reason} -> {:discard, {:invalid_email, reason}} |
| - | {:error, %{code: "InvalidParameterValue"} = result} -> |
| - | {:discard, {:invalid_param, result}} |
| - | {:error, result} -> {:error, result} |
| - | end |
| - | end |
| - | |
| - | # Pattern matching makes this check trivial |
| - | defp ensure_campaign_email_not_sent(%CampaignEmail{sent_at: nil}), do: {:ok} |
| - | defp ensure_campaign_email_not_sent(%CampaignEmail{sent_at: _}), do: {:already_sent} |
| - | end |
| - | ``` |
| - | |
| - | **Read the `else` block like a specification:** |
| - | |
| - | - Rate exceeded? → Snooze 1 second, try again |
| - | - Daily limit hit? → Snooze with exponential backoff, up to 1 hour |
| - | - Already sent? → Discard (don't double-send) |
| - | - Invalid email? → Discard (don't retry what won't work) |
| - | - AWS rejected the parameters? → Discard |
| - | - Any other error? → Retry (Oban handles retry logic) |
| - | |
| - | Every possible outcome is handled. No silent failures. No lost emails. |
| - | |
| - | ### Scheduling 500k Jobs Efficiently |
| - | |
| - | You can't load 500k records into memory at once. Here's how the scheduler |
| - | streams them in chunks: |
| - | |
| - | <!-- livebook:{"force_markdown":true} --> |
| - | |
| - | ```elixir |
| - | defmodule Marketing.CampaignSchedulerWorker do |
| - | use Oban.Worker, queue: :default, max_attempts: 1 |
| - | |
| - | @chunk_size 2_000 |
| - | |
| - | def perform(%Oban.Job{args: args}) do |
| - | campaign = Campaigns.get_campaign_with_site(args["campaign_id"]) |
| - | |
| - | Repo.transaction(fn -> |
| - | Repo.stream(CampaignMailer.campaign_member_ids_query(campaign), max_rows: @chunk_size) |
| - | |> Stream.chunk_every(@chunk_size) |
| - | |> Enum.each(&schedule_batch(&1, campaign, args["live_mode"])) |
| - | end, timeout: :infinity) |
| - | end |
| - | |
| - | defp schedule_batch(member_ids, campaign, live_mode) do |
| - | Enum.map(member_ids, fn member_id -> |
| - | %{campaign_id: campaign.id, member_id: member_id, live_mode: live_mode} |
| - | |> CampaignMailerWorker.new(scheduled_at: campaign.scheduled_at) |
| - | end) |
| - | |> Oban.insert_all() |
| - | end |
| - | end |
| - | ``` |
| - | |
| - | `Repo.stream` + `Stream.chunk_every` = process 500k records using constant memory. |
| - | Each chunk of 2,000 member IDs becomes 2,000 individual email jobs. |
| - | |
| - | ## 3.2 — Domain Registry: TCP Connections to Verisign |
| - | |
| - | ### The Challenge |
| - | |
| - | Our domain registrar app (OrbitFour) needs to communicate directly with |
| - | **Verisign's EPP** (Extensible Provisioning Protocol) — the system that |
| - | actually registers `.com` and `.net` domains. |
| - | |
| - | This means: |
| - | - **Persistent TCP/TLS connections** to Verisign's servers |
| - | - **XML over binary framing** (RFC 5734 — 4-byte length header + XML payload) |
| - | - **Connection pooling** — maintain authenticated sessions, reuse them efficiently |
| - | - **Automatic recovery** — if a connection drops, reconnect without manual intervention |
| - | |
| - | ### The Connection — `with` Chains and Binary Pattern Matching |
| - | |
| - | Here's how we establish a connection. Notice the `with` chain — the same pattern |
| - | we saw in the email worker: |
| - | |
| - | <!-- livebook:{"force_markdown":true} --> |
| - | |
| - | ```elixir |
| - | defmodule OrbitFour.Epp.Connection do |
| - | defstruct [:socket, :host, :port, :logged_in, :session_id, :greeting] |
| - | |
| - | def connect(host, port, username, password, opts \\ []) do |
| - | with {:ok, socket_info} <- establish_ssl_connection(host, port, opts, timeout), |
| - | {:ok, greeting} <- read_greeting(socket_info, timeout), |
| - | :ok <- validate_greeting(greeting), |
| - | {:ok, session_id} <- perform_login(socket_info, username, password, timeout) do |
| - | {:ok, %__MODULE__{ |
| - | socket: socket_info, host: host, port: port, |
| - | logged_in: true, session_id: session_id, greeting: greeting |
| - | }} |
| - | else |
| - | {:error, reason} = error -> |
| - | Logger.error("EPP connection failed to #{host}:#{port} - #{inspect(reason)}") |
| - | error |
| - | end |
| - | end |
| - | end |
| - | ``` |
| - | |
| - | Four steps: connect → read greeting → validate → login. If any step fails, |
| - | it short-circuits with an error. Clean, linear, no nesting. |
| - | |
| - | ### Binary Pattern Matching — Parsing Network Protocols |
| - | |
| - | This is something you **can't easily do** in most languages. Elixir can pattern |
| - | match on individual bytes and bits: |
| - | |
| - | <!-- livebook:{"force_markdown":true} --> |
| - | |
| - | ```elixir |
| - | # Sending: build a frame with a 4-byte length header (big-endian) |
| - | def send_frame(socket_info, xml_data) do |
| - | xml_bytes = :unicode.characters_to_binary(xml_data, :unicode, :utf8) |
| - | total_length = byte_size(xml_bytes) + 4 |
| - | |
| - | # This line creates the binary frame: |
| - | # <<total_length::32-big>> = 4 bytes, big-endian (network byte order) |
| - | # followed by the XML payload |
| - | frame = <<total_length::32-big, xml_bytes::binary>> |
| - | |
| - | send_data(socket_info, frame) |
| - | end |
| - | |
| - | # Receiving: parse the 4-byte header to know how many bytes to read |
| - | def receive_frame(socket_info, timeout) do |
| - | with {:ok, <<length::32-big>>} <- recv_data(socket_info, 4, timeout) do |
| - | data_length = length - 4 |
| - | recv_data(socket_info, data_length, timeout) |
| - | end |
| - | end |
| - | ``` |
| - | |
| - | `<<total_length::32-big>>` — that's Elixir saying "4 bytes, big-endian integer." |
| - | It works at the **bit level**. In JavaScript or Ruby, you'd be manually slicing |
| - | buffers and calling `readUInt32BE`. Here, it's just pattern matching. |
| - | |
| - | ### The Connection Pool — Supervised Processes |
| - | |
| - | Each connection to Verisign is a long-lived TCP session. The connection pool |
| - | manages them as supervised processes: |
| - | |
| - | <!-- livebook:{"force_markdown":true} --> |
| - | |
| - | ```elixir |
| - | defmodule OrbitFour.Epp.ConnectionPool do |
| - | use GenServer |
| - | |
| - | @pool_size 2 # Connections per registry |
| - | @max_overflow 3 # Extra connections under load |
| - | @max_connection_age 4 * 60 * 60 * 1000 # 4 hours |
| - | |
| - | # The public API — checkout a connection, do work, return it |
| - | def with_connection(registrar_module, fun, opts \\ []) do |
| - | timeout = Keyword.get(opts, :timeout, 30_000) |
| - | |
| - | try do |
| - | GenServer.call(__MODULE__, {:checkout, registrar_module, timeout}, timeout + 5_000) |
| - | |> case do |
| - | {:ok, conn} -> |
| - | try do |
| - | result = fun.(conn) |
| - | GenServer.cast(__MODULE__, {:checkin, registrar_module, conn, :ok}) |
| - | result |
| - | rescue |
| - | error -> |
| - | GenServer.cast(__MODULE__, {:checkin, registrar_module, conn, :error}) |
| - | reraise error, __STACKTRACE__ |
| - | end |
| - | |
| - | {:error, :pool_exhausted} -> |
| - | {:error, "Connection pool is busy. Please try again."} |
| - | end |
| - | catch |
| - | :exit, {:timeout, _} -> |
| - | {:error, "Registry is taking too long to respond."} |
| - | end |
| - | end |
| - | end |
| - | ``` |
| - | |
| - | **How it works:** |
| - | |
| - | - `checkout` → get a healthy connection from the pool |
| - | - Execute your function with that connection |
| - | - `checkin` with `:ok` → return it to the pool for reuse |
| - | - `checkin` with `:error` → close the bad connection, pool creates a new one later |
| - | - If the pool is empty, create a new connection (up to `max_overflow`) |
| - | - If everything is busy, return a user-friendly error |
| - | |
| - | ### Pattern Matching for Health Checks |
| - | |
| - | The pool checks connection health. Notice how pattern matching handles |
| - | every possible socket state: |
| - | |
| - | <!-- livebook:{"force_markdown":true} --> |
| - | |
| - | ```elixir |
| - | # No socket — unhealthy |
| - | defp connection_healthy?(%Connection{socket: nil}), do: false |
| - | |
| - | # Not logged in — unhealthy |
| - | defp connection_healthy?(%Connection{logged_in: false}), do: false |
| - | |
| - | # SSL socket — check with the SSL module |
| - | defp connection_healthy?(%Connection{socket: {:ssl, socket}}) do |
| - | case :ssl.connection_information(socket) do |
| - | {:ok, _info} -> true |
| - | _error -> false |
| - | end |
| - | end |
| - | |
| - | # TCP socket — check with the inet module |
| - | defp connection_healthy?(%Connection{socket: {:tcp, socket}}) do |
| - | case :inet.peername(socket) do |
| - | {:ok, _} -> true |
| - | {:error, _} -> false |
| - | end |
| - | end |
| - | |
| - | # Unknown socket type — unhealthy |
| - | defp connection_healthy?(_conn), do: false |
| - | ``` |
| - | |
| - | Five function clauses, zero if/else. Each one handles exactly one case. |
| - | The compiler ensures you haven't missed anything. |
| - | |
| - | ### The Full Supervision Tree |
| - | |
| - | Here's OrbitFour's `application.ex` — one supervisor managing everything: |
| - | |
| - | <!-- livebook:{"force_markdown":true} --> |
| - | |
| - | ```elixir |
| - | defmodule OrbitFour.Application do |
| - | use Application |
| - | |
| - | def start(_type, _args) do |
| - | children = [ |
| - | OrbitFourWeb.Telemetry, |
| - | OrbitFour.Repo, # Database |
| - | {Phoenix.PubSub, name: OrbitFour.PubSub}, |
| - | {Finch, name: OrbitFour.Finch}, # HTTP client |
| - | {Oban, ...}, # Job queue |
| - | OrbitFour.Epp.ConnectionPool, # ← TCP connection pool |
| - | OrbitFour.Epp.HealthCache, # ← Periodic health checks |
| - | OrbitFour.Billing.ExchangeRates, # ← Currency rates cache |
| - | OrbitFour.Rdap.Cache, # ← WHOIS data cache |
| - | OrbitFourWeb.Endpoint # Web server |
| - | ] |
| - | |
| - | opts = [strategy: :one_for_one, name: OrbitFour.Supervisor] |
| - | Supervisor.start_link(children, opts) |
| - | end |
| - | end |
| - | ``` |
| - | |
| - | Database, web server, TCP connection pool, health monitors, caches, job queue — |
| - | all running as supervised processes in a single application. |
| - | |
| - | If the connection pool crashes (bad TCP state, network blip), the supervisor |
| - | restarts it. New connections are established. The app keeps running. |
| - | No pager alerts. No manual restarts. |
| - | |
| - | ## 3.3 — Internal Admin: Multi-Site LiveView Dashboard |
| - | |
| - | ### The Philosophy |
| - | |
| - | At Vianet, we manage multiple web properties. Instead of building admin features |
| - | **into** each app (polluting the customer-facing codebase), we built a |
| - | **separate admin app** that connects to all of them. |
| - | |
| - | Benefits: |
| - | - **Customer apps stay focused** — no admin routes, no admin UI, no admin auth |
| - | - **Admin app iterates fast** — internal tool, no public-facing risk |
| - | - **One dashboard** — manage all sites from a single place |
| - | - **LiveView** — real-time updates, interactive tables, presence tracking |
| - | |
| - | ### Multi-Database Architecture |
| - | |
| - | The admin app's supervision tree connects to **five databases** simultaneously: |
| - | |
| - | <!-- livebook:{"force_markdown":true} --> |
| - | |
| - | ```elixir |
| - | defmodule VianetAdmin.Application do |
| - | use Application |
| - | |
| - | def start(_type, _args) do |
| - | children = [ |
| - | VianetAdminWeb.Telemetry, |
| - | VianetAdmin.Repo, # Admin's own database |
| - | VianetAdmin.PuppiesRepo, # Puppies.com database |
| - | VianetAdmin.RRRepo, # ReputableRooms database |
| - | VianetAdmin.RoommatesRepo, # Roommates database |
| - | VianetAdmin.GeocodeRepo, # Geocoding database |
| - | VianetAdmin.ConnectionMonitor, |
| - | {Phoenix.PubSub, name: VianetAdmin.PubSub}, |
| - | {Finch, name: VianetAdmin.Finch}, |
| - | VianetAdmin.Presence, # ← Track which admins are online |
| - | VianetAdminWeb.Endpoint, |
| - | VianetAdmin.Scheduler, |
| - | {Oban, ...}, |
| - | {Cachex, [:cache]} # In-memory cache |
| - | ] |
| - | |
| - | opts = [strategy: :one_for_one, name: VianetAdmin.Supervisor] |
| - | Supervisor.start_link(children, opts) |
| - | end |
| - | end |
| - | ``` |
| - | |
| - | Each `Repo` is a supervised connection pool to a different PostgreSQL database. |
| - | The admin app can query any of them with the appropriate Repo module. |
| - | |
| - | ### LiveView: Real-Time Admin Forms |
| - | |
| - | Here's a real LiveView from the admin app — a form that manages admin accounts |
| - | across all sites, loading data from three databases asynchronously: |
| - | |
| - | <!-- livebook:{"force_markdown":true} --> |
| - | |
| - | ```elixir |
| - | defmodule VianetAdminWeb.AdminsFormLive do |
| - | use VianetAdminWeb, :live_view |
| - | |
| - | def mount(params, _session, socket) do |
| - | {form, action} = |
| - | if Map.has_key?(params, "id") do |
| - | admin = Accounts.get_admin!(params["id"]) |
| - | {Admin.update_changeset(admin, %{}) |> to_form(), :edit} |
| - | else |
| - | {Admin.create_changeset(%Admin{}, %{}) |> to_form(), :new} |
| - | end |
| - | |
| - | socket = |
| - | socket |
| - | |> assign(form: form, action: action) |
| - | |> assign_async( |
| - | [:puppies_admins, :roommates_admins, :reputable_rooms_admins], |
| - | fn -> |
| - | {:ok, %{ |
| - | puppies_admins: Puppies.Admins.get_active_admins(), |
| - | roommates_admins: Roommates.Admins.get_admins(), |
| - | reputable_rooms_admins: ReputableRooms.Admins.get_admins() |
| - | }} |
| - | end |
| - | ) |
| - | |
| - | {:ok, socket} |
| - | end |
| - | |
| - | def handle_event("submit", %{"admin" => admin}, socket) do |
| - | res = |
| - | if socket.assigns.action == :new do |
| - | Accounts.create_admin(admin) |
| - | else |
| - | Accounts.update_admin(socket.assigns.form.data, admin) |
| - | end |
| - | |
| - | case res do |
| - | {:ok, admin} -> |
| - | {:noreply, |
| - | socket |> redirect(to: ~p"/admins") |> put_flash(:info, "Admin saved")} |
| - | |
| - | {:error, %Ecto.Changeset{} = changeset} -> |
| - | {:noreply, assign(socket, :form, changeset |> to_form())} |
| - | end |
| - | end |
| - | end |
| - | ``` |
| - | |
| - | **Key patterns:** |
| - | |
| - | - `assign_async` — loads admin lists from 3 databases **concurrently**, shows loading states automatically |
| - | - `handle_event("submit", ...)` — form submission is an Elixir function, not a REST endpoint |
| - | - Pattern matching on `{:ok, admin}` vs `{:error, changeset}` — success goes to redirect, error re-renders form with validation errors |
| - | |
| - | ### LiveView: Real-Time Presence |
| - | |
| - | When multiple admins are looking at the same user record, they can see |
| - | each other in real-time: |
| - | |
| - | <!-- livebook:{"force_markdown":true} --> |
| - | |
| - | ```elixir |
| - | # In mount — track this admin's presence and subscribe to changes |
| - | Presence.track(self(), "admin:presence", admin.id, %{ |
| - | initials: Utilities.initials(admin), |
| - | current_user: params["id"] |
| - | }) |
| - | |
| - | Phoenix.PubSub.subscribe(PubSub, "admin:presence") |
| - | |
| - | # Handle presence changes — who joined, who left |
| - | def handle_info( |
| - | %Phoenix.Socket.Broadcast{event: "presence_diff", payload: diff}, |
| - | socket) do |
| - | {:noreply, |
| - | socket |
| - | |> handle_leaves(diff.leaves) |
| - | |> handle_joins(diff.joins)} |
| - | end |
| - | |
| - | defp handle_joins(socket, joins) do |
| - | Enum.reduce(joins, socket, fn {admin, %{metas: [meta | _]}}, socket -> |
| - | assign(socket, :admins, Map.put(socket.assigns.admins, admin, meta)) |
| - | end) |
| - | end |
| - | ``` |
| - | |
| - | **No WebSocket code. No JavaScript. No polling.** Phoenix Presence handles |
| - | distributed presence tracking across all connected clients automatically. |
| - | |
| - | ### LiveView: Async Data with Real-Time Charts |
| - | |
| - | Here's a stats dashboard that loads data asynchronously and pushes chart |
| - | updates to the browser: |
| - | |
| - | <!-- livebook:{"force_markdown":true} --> |
| - | |
| - | ```elixir |
| - | defmodule VianetAdminWeb.Roommates.StatsLive do |
| - | use VianetAdminWeb, :live_view |
| - | |
| - | def mount(_params, _session, socket) do |
| - | socket = |
| - | socket |
| - | |> assign(stats: AsyncResult.loading()) |
| - | |> assign_async(:site_stats, fn -> |
| - | {:ok, %{site_stats: VianetAdmin.Stats.latest_by_site("roommates")}} |
| - | end) |
| - | |> start_async(:stats, fn -> Stats.load_stats("havers", "7") end) |
| - | |
| - | {:ok, socket} |
| - | end |
| - | |
| - | # When the async task completes, update the chart |
| - | def handle_async(:stats, {:ok, fetched_stats}, socket) do |
| - | data = %{ |
| - | stats: fetched_stats, |
| - | title: "Number of listings created per hour" |
| - | } |
| - | |
| - | socket = |
| - | socket |
| - | |> assign(stats: AsyncResult.ok(socket.assigns.stats, fetched_stats)) |
| - | |
| - | {:noreply, push_event(socket, "graph", data)} |
| - | end |
| - | |
| - | # User changes the filter — reload asynchronously |
| - | def handle_event("update-stats", %{"table" => table, "range" => range}, socket) do |
| - | socket = |
| - | socket |
| - | |> assign(table: table, range: range, stats: AsyncResult.loading()) |
| - | |> start_async(:stats, fn -> Stats.load_stats(table, range) end) |
| - | |
| - | {:noreply, socket} |
| - | end |
| - | end |
| - | ``` |
| - | |
| - | **The flow:** |
| - | |
| - | 1. Page loads → shows loading spinner (`AsyncResult.loading()`) |
| - | 2. Async task completes → `handle_async` fires, data flows to chart via `push_event` |
| - | 3. User changes filter → new async task starts, loading spinner appears |
| - | 4. New data arrives → chart updates in real-time |
| - | |
| - | No REST API. No `fetch()`. No loading state management library. |
| - | All handled by LiveView's built-in async primitives. |
| - | |
| - | ## What We've Seen |
| - | |
| - | Three production systems, same patterns: |
| - | |
| - | | Pattern | Email System | Domain Registry | Admin App | |
| - | |---------|-------------|-----------------|-----------| |
| - | | **Supervision** | Rate limiter + job queue | Connection pool + health cache | Multi-DB + presence + cache | |
| - | | **GenServer** | Token bucket rate limiter | TCP connection pool | Connection monitor | |
| - | | **Pattern matching** | Error handling in `with` | Socket type dispatch | Form actions, presence diffs | |
| - | | **`with` chains** | Email pipeline (6 steps) | Connect → greet → login | Form submit → redirect | |
| - | | **Concurrency** | 500k jobs in parallel | Pooled TCP connections | Async data loading from 5 DBs | |
| - | | **LiveView** | Campaign management UI | Domain search & registration | Real-time dashboards + presence | |
| - | |
| - | These aren't separate tools bolted together. They're all **built into the language |
| - | and framework**. One supervision tree. One deployment. One codebase. |
| - | |
| - | ## Wrap-up |
| - | |
| - | ### What We Covered Today |
| - | |
| - | 1. **Why Elixir exists** — Erlang's 40-year legacy of reliability, made accessible |
| - | 2. **Pattern matching** — write clearer code where every case is explicit |
| - | 3. **Processes & supervision** — concurrency without locks, fault tolerance without fear |
| - | 4. **LiveView** — real-time web apps without JavaScript complexity |
| - | 5. **Real production systems** — 500k emails, TCP protocol integration, multi-site admin tools |
| - | |
| - | ### The Pitch |
| - | |
| - | Elixir isn't just "another language." It changes **how you think** about building web apps: |
| - | |
| - | - State lives in processes, not shared memory |
| - | - Errors are handled by design, not by hope |
| - | - Concurrency is the default, not an afterthought |
| - | - Real-time features come for free |
| - | |
| - | ### Getting Started |
| - | |
| - | - **Elixir:** [elixir-lang.org](https://elixir-lang.org) — the official guides are excellent |
| - | - **Livebook:** [livebook.dev](https://livebook.dev) — install it tonight, start experimenting |
| - | - **Phoenix:** [phoenixframework.org](https://phoenixframework.org) — the web framework |
| - | - **Elixir Forum:** [elixirforum.com](https://elixirforum.com) — one of the friendliest communities in tech |
| - | - **Elixir School:** [elixirschool.com](https://elixirschool.com) — free lessons, great for self-paced learning |
| - | |
| - | ### Thank You |
| - | |
| - | **Torey Heinz** |
| - | Vianet Management / Shopflow |
| - | |
| - | *Questions?* |
outline.md
+0
-208
| @@ | @@ -1,208 +0,0 @@ |
| - | # Elixir in the Real World: A Practical Introduction |
| - | |
| - | **Presenter:** Torey Heinz |
| - | **Event:** GR Web Dev Meetup — February 23, 2026 |
| - | **Format:** ~45-50 min presentation + 10-15 min Q&A |
| - | **Demo Tool:** Livebook (interactive code notebooks) |
| - | |
| - | ### Presentation Format: Livebook Notebooks |
| - | |
| - | The entire presentation is built as Livebook `.livemd` files — one per major section: |
| - | |
| - | | File | Content | Style | |
| - | |------|---------|-------| |
| - | | `01-intro.livemd` | Introduction + Part 1: Why Elixir | Mostly markdown, light on code | |
| - | | `02-the-language.livemd` | Part 2: Pattern Matching, Processes, LiveView | Mix of markdown + live code demos | |
| - | | `03-real-world.livemd` | Part 3: Email system, Verisign, Admin app | Code walkthroughs + production examples | |
| - | |
| - | **Why this structure:** |
| - | - Each notebook stays focused and manageable |
| - | - Code cells can be pre-run or executed live during the talk |
| - | - If a demo crashes, only that notebook's session is affected |
| - | - Natural transition points between parts |
| - | - Livebook's **Presentation View** (v0.10+) hides sidebar chrome and focuses on content |
| - | - Each `## Section` within a notebook acts as a logical "slide" |
| - | - The audience sees Livebook itself — a tool they can install and use immediately |
| - | |
| - | --- |
| - | |
| - | ## Introduction (2-3 min) |
| - | |
| - | - Who I am: Full-stack developer, 15+ years building web apps |
| - | - Shout out to **Vianet Management** (vianetmanagement.com) — my employer who has fully embraced Elixir and given us the freedom to adopt it across our stack |
| - | - Founder of **Shopflow** — software for small manufacturing businesses |
| - | - My path: Long-time Ruby on Rails developer who caught the Elixir bug and hasn't looked back |
| - | - What we're covering today: Why Elixir exists, what makes it click, and real production code from my day job |
| - | |
| - | --- |
| - | |
| - | ## Part 1: Why Elixir? (10-12 min) |
| - | *Hook them with the "so what" before diving into syntax* |
| - | |
| - | ### 1.1 The Origin Story (3 min) |
| - | |
| - | - **Erlang (1986):** Built by Ericsson for telecom switches — systems that literally could not go down |
| - | - Designed for: concurrency, distribution, fault tolerance, hot code upgrades |
| - | - Battle-tested for 40 years in phone networks, banking, messaging |
| - | - **The BEAM:** Erlang's virtual machine — purpose-built for running millions of lightweight processes |
| - | - Not a JVM, not an interpreter — a VM designed from scratch for concurrent, fault-tolerant systems |
| - | - **Elixir (2012):** José Valim, a Ruby on Rails core team member, brought modern developer ergonomics to the BEAM |
| - | - Ruby-inspired syntax, excellent tooling (Mix, Hex, ExUnit) |
| - | - Full access to the entire Erlang ecosystem |
| - | - "What if we took the best runtime in the world and made it a joy to write?" |
| - | |
| - | ### 1.2 Who's Using It (2 min) |
| - | |
| - | - **Discord:** 5+ million concurrent users, real-time messaging at massive scale |
| - | - **Pinterest:** Notification delivery system handling billions of events |
| - | - **Pepsi:** Real-time manufacturing and logistics systems |
| - | - The famous Phoenix demo: **2 million simultaneous WebSocket connections on a single server** |
| - | - Not just startups — companies choose Elixir when reliability and scale actually matter |
| - | |
| - | ### 1.3 Why It Matters for Web Apps (5 min) |
| - | |
| - | - **The key insight:** Web applications are fundamentally I/O bound |
| - | - Your app spends most of its time *waiting* — on databases, APIs, file systems, user input |
| - | - Most languages: threads are expensive, blocking is painful, concurrency is an afterthought |
| - | - Elixir: lightweight processes make concurrency the default, not the exception |
| - | - **Real impact at Vianet:** |
| - | - Migrated services from Ruby on Rails on AWS to Elixir on Render |
| - | - Result: faster response times, fewer resources, **~$10k/month infrastructure savings** |
| - | - Not because Rails is bad — because Elixir's concurrency model is a natural fit for what web apps actually do |
| - | |
| - | --- |
| - | |
| - | ## Part 2: The Language (15-18 min) |
| - | *Not a full tutorial — just the "aha" moments that make Elixir click* |
| - | |
| - | ### 2.1 Pattern Matching (6-8 min) |
| - | |
| - | - **The shift:** `=` isn't assignment — it's pattern matching |
| - | - Destructuring built into the language at every level |
| - | - *Livebook demo:* basic pattern matching with tuples, maps, lists |
| - | - **Function heads:** Same function name, different clauses based on input shape |
| - | - Each clause handles one specific case — no `if/else` chains, no switch statements |
| - | - The code reads like a specification |
| - | - *Livebook demo:* handling different API response shapes with multiple function heads |
| - | - **Personal story:** "At first it didn't click. Then I realized I was writing clearer code with fewer lines, and every function clause told me exactly what case it handled." |
| - | - **The payoff for the audience:** Think about how you handle API responses, form validation, state transitions — pattern matching transforms all of these |
| - | |
| - | ### 2.2 Processes & Concurrency (5-6 min) |
| - | |
| - | - **Processes are cheap:** Not OS threads — BEAM processes use ~2KB of memory each |
| - | - You can spin up millions on a single machine |
| - | - Each process has its own heap, its own garbage collection — no stop-the-world pauses |
| - | - **Message passing:** Processes communicate by sending messages, not sharing state |
| - | - No locks, no mutexes, no race conditions by design |
| - | - *Livebook demo:* spawning processes, sending messages |
| - | - **"Let it crash" philosophy:** |
| - | - Instead of defensive programming, let processes fail and have supervisors restart them |
| - | - Supervision trees: parent processes monitor children, define restart strategies |
| - | - Like having a manager who automatically fixes problems instead of writing code to handle every possible failure |
| - | - *Livebook demo:* simple supervisor example |
| - | |
| - | ### 2.3 LiveView (4-5 min) |
| - | |
| - | - **The pitch:** Real-time, interactive UI without writing JavaScript |
| - | - **How it works:** |
| - | - Initial page load: regular HTML (SEO-friendly, fast first paint) |
| - | - Then: WebSocket connection upgrades the page to real-time |
| - | - User interaction → client sends the event + minimal data over WebSocket |
| - | - Server processes the event, re-renders, sends back only the HTML diff |
| - | - Result: tiny payloads, instant updates, server-rendered state of truth |
| - | - **Why this matters:** |
| - | - No REST API to build and maintain |
| - | - No client-side state management (Redux, Zustand, etc.) |
| - | - No JSON serialization/deserialization layer |
| - | - Real-time features (live updates, presence, forms) come nearly for free |
| - | - **Transition to Part 3:** "Let me show you what this looks like in production..." |
| - | |
| - | --- |
| - | |
| - | ## Part 3: Real-World Code (15-18 min) |
| - | *Prove the concepts with battle-tested production examples* |
| - | |
| - | ### 3.1 Email Marketing System (6-8 min) |
| - | |
| - | - **The challenge:** |
| - | - Send 500k+ personalized marketing emails through AWS SES |
| - | - The catch: if you exceed your rate limit, AWS doesn't tell you — they just silently drop your emails |
| - | - Need: reliable delivery with rate limiting, personalization, and webhook processing |
| - | - **How Elixir solves it:** |
| - | - Supervision tree managing the entire pipeline |
| - | - Rate limiter process that enforces SES limits — no silent drops |
| - | - Concurrent email processing: personalize, render, send — all in parallel within the rate limit |
| - | - Webhook processing: handle bounces, complaints, deliveries concurrently |
| - | - **Code walkthrough:** |
| - | - Show the supervision tree structure |
| - | - Show rate limiting with GenServer |
| - | - Show how pattern matching makes webhook handling clean |
| - | |
| - | ### 3.2 Domain Registry Integration (4-5 min) |
| - | |
| - | - **The challenge:** |
| - | - Manage domain registrations directly with Verisign's EPP (Extensible Provisioning Protocol) |
| - | - Persistent TCP connections required — XML over TLS |
| - | - Connection pooling: maintain a pool of authenticated sessions |
| - | - **How Elixir solves it:** |
| - | - BEAM processes map perfectly to persistent connections |
| - | - Each connection is a supervised process — if it drops, the supervisor reconnects |
| - | - Connection pooling is natural with process-based architecture |
| - | - **Code walkthrough:** |
| - | - Show the connection GenServer |
| - | - Show how supervision handles connection failures gracefully |
| - | |
| - | ### 3.3 Internal Admin App (4-5 min) |
| - | |
| - | - **The philosophy:** |
| - | - Keep your main app focused on delivering business value to customers |
| - | - Build admin tools as a separate app — don't pollute your core codebase |
| - | - Internal tools can move fast, iterate freely, without risking customer-facing code |
| - | - **What we built:** |
| - | - Multi-site management dashboard across Vianet properties |
| - | - Real-time admin interfaces with LiveView |
| - | - Interactive data views, filtering, bulk operations — all server-rendered, minimal JavaScript |
| - | - **The speed of development:** |
| - | - LiveView lets you build interactive admin tools remarkably fast |
| - | - No separate frontend build pipeline, no API layer to maintain |
| - | - Changes deploy and go live instantly for all connected users |
| - | |
| - | --- |
| - | |
| - | ## Wrap-up (2-3 min) |
| - | |
| - | - **Recap the arc:** |
| - | - The BEAM gives you a battle-tested foundation for concurrency and fault tolerance |
| - | - Elixir makes that foundation accessible and enjoyable to work with |
| - | - Pattern matching, processes, and LiveView change how you think about building web apps |
| - | - These aren't toy examples — this is production code running real businesses |
| - | - **Getting started:** |
| - | - [elixir-lang.org](https://elixir-lang.org) — official guides are excellent |
| - | - [Livebook](https://livebook.dev) — install it tonight, start experimenting |
| - | - [Phoenix Framework](https://phoenixframework.org) — the web framework |
| - | - Elixir Forum — one of the friendliest communities in tech |
| - | - **Contact / Q&A** |
| - | - Your info, socials, how to reach you |
| - | - Open the floor for questions |
| - | |
| - | --- |
| - | |
| - | ## Presenter Notes |
| - | |
| - | ### Narrative Thread |
| - | The story arc is: **History → Understanding → Proof**. Part 1 gives them a reason to care, Part 2 gives them the "aha" moments, Part 3 proves it works in the real world. Each section builds trust. |
| - | |
| - | ### Livebook Strategy |
| - | Using Livebook for all code demos serves double duty — it demonstrates the code AND shows off the tooling. The audience sees a tool they can install and use immediately. |
| - | |
| - | ### Audience Awareness |
| - | This is a JS/Rails crowd. Lean into comparisons they'll recognize: |
| - | - Pattern matching vs. switch statements / if-else chains |
| - | - BEAM processes vs. Node.js event loop / threads |
| - | - LiveView vs. React + API + WebSocket libraries |
| - | - Supervision trees vs. try/catch + process managers |
| - | |
| - | ### Timing Checkpoints |
| - | - After Part 1 (~12 min): Should feel energized, curious |
| - | - After Part 2 (~30 min): Should feel like they "get" what's different about Elixir |
| - | - After Part 3 (~48 min): Should feel like Elixir is real, practical, worth trying |