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