Trim presentation for 50-minute time budget

Torey Heinz committed Feb 23, 2026
commit 3cca52f9f500e36a16d2629ff010edaac72f1eef
Showing 4 changed files with 76 additions and 414 deletions
.gitignore +1 -0
@@ @@ -0,0 +1 @@
+ dev/walkthrough
02-the-language.livemd +14 -185
@@ @@ -13,13 +13,7 @@ We'll cover three things:
## Pattern Matching: The Basics
In most languages, `=` means **assignment**. In Elixir, `=` means **match**.
-
- ```elixir
- # This isn't "assign 1 to x" — it's "match x to 1"
- x = 1
- ```
-
- Seems the same, right? But watch what happens with more complex data:
+ It works like destructuring, but it also **validates the shape** of your data.
```elixir
# Destructure a tuple
@@ @@ -143,21 +137,11 @@ end
ApiHandler.handle_response(%{status: 200, body: %{users: ["Alice", "Bob"]}})
```
- ```elixir
- ApiHandler.handle_response(%{status: 404})
- ```
-
```elixir
ApiHandler.handle_response(%{status: 429, headers: %{"retry-after" => "30"}})
```
- ```elixir
- ApiHandler.handle_response(%{status: 503})
- ```
-
- Notice: **no if/else chains, no switch statements**. Each clause is self-documenting.
- When you come back to this code in 6 months, each function head tells you exactly
- what case it handles.
+ No `if/else` chains, no `switch` statements. Each clause is self-documenting.
## Pattern Matching: The `with` Statement
@@ @@ -166,10 +150,12 @@ Real-world code often has multiple steps that can each fail. In other languages
```elixir
defmodule OrderProcessor do
def process_order(order_id) do
- with {:ok, order} <- find_order(order_id),
+ with(
+ {:ok, order} <- find_order(order_id),
{:ok, _} <- validate_inventory(order),
{:ok, charge} <- charge_payment(order),
- {:ok, shipment} <- create_shipment(order) do
+ {:ok, shipment} <- create_shipment(order)
+ ) do
{:ok, %{order: order, charge: charge, shipment: shipment}}
else
{:error, :not_found} -> {:error, "Order not found"}
@@ @@ -201,32 +187,17 @@ OrderProcessor.process_order(1)
```
```elixir
- # Order not found — short circuits immediately
- OrderProcessor.process_order(999)
- ```
-
- ```elixir
- # Out of stock — fails at validate_inventory step
+ # Out of stock — short circuits at validate_inventory
OrderProcessor.process_order(2)
```
- ```elixir
- # Payment declined — fails at charge_payment step
- OrderProcessor.process_order(3)
- ```
-
The `with` statement chains operations that return `{:ok, value}` or `{:error, reason}`.
If any step fails, it jumps straight to the `else` block.
- **For JS devs:** This is like chaining `.then()` calls, but without callbacks and with
- pattern-matched error handling built in.
-
- **For Rails devs:** This replaces the "service object with early returns" pattern.
-
## Processes: The Heart of Elixir
- Everything in Elixir runs inside **processes**. Not OS processes, not threads —
- BEAM processes. They're incredibly lightweight (~2KB each) and completely isolated.
+ Everything in Elixir runs inside **processes** — not OS processes, not threads.
+ They're ~2KB each, completely isolated, with their own memory and garbage collection.
```elixir
# Spawn a process — it runs concurrently
@@ @@ -238,7 +209,7 @@ IO.puts("Hello from the main process #{inspect(self())}")
```
```elixir
- # You can spawn MILLIONS of these. Let's spawn 10,000 just to show it's trivial.
+ # Let's spawn 10,000 just to show it's trivial.
{time_microseconds, _} =
:timer.tc(fn ->
1..10_000
@@ @@ -250,90 +221,6 @@ IO.puts("Hello from the main process #{inspect(self())}")
IO.puts("Spawned 10,000 processes in #{time_microseconds / 1_000}ms")
```
- **Key insight:** Each process has its own memory, its own garbage collection.
- One process can't corrupt another's state. If a process crashes, nothing else is affected.
-
- ## Processes: GenServer — Stateful Processes
-
- When you need a process that **maintains state** and **responds to messages**,
- you use a GenServer. Think of it as a tiny, isolated server running inside your app.
-
- Here's a simple counter — but the pattern is the same one we use for
- rate limiters, connection pools, and caches in production:
-
- ```elixir
- defmodule Counter do
- use GenServer
-
- # --- Client API (what other code calls) ---
-
- def start_link(initial_value \\ 0) do
- GenServer.start_link(__MODULE__, initial_value)
- end
-
- def increment(pid), do: GenServer.call(pid, :increment)
- def decrement(pid), do: GenServer.call(pid, :decrement)
- def get_value(pid), do: GenServer.call(pid, :get)
- def reset(pid), do: GenServer.cast(pid, :reset)
-
- # --- Server Callbacks (runs inside the process) ---
-
- @impl true
- def init(initial_value) do
- {:ok, initial_value}
- end
-
- # call = synchronous (caller waits for reply)
- @impl true
- def handle_call(:increment, _from, count) do
- new_count = count + 1
- {:reply, new_count, new_count}
- end
-
- @impl true
- def handle_call(:decrement, _from, count) do
- new_count = count - 1
- {:reply, new_count, new_count}
- end
-
- @impl true
- def handle_call(:get, _from, count) do
- {:reply, count, count}
- end
-
- # cast = asynchronous (fire and forget)
- @impl true
- def handle_cast(:reset, _count) do
- {:noreply, 0}
- end
- end
- ```
-
- ```elixir
- {:ok, counter} = Counter.start_link(0)
-
- Counter.increment(counter) |> IO.inspect(label: "After increment")
- Counter.increment(counter) |> IO.inspect(label: "After increment")
- Counter.increment(counter) |> IO.inspect(label: "After increment")
- Counter.decrement(counter) |> IO.inspect(label: "After decrement")
- Counter.get_value(counter) |> IO.inspect(label: "Current value")
- Counter.reset(counter)
- Counter.get_value(counter) |> IO.inspect(label: "After reset")
- ```
-
- **Why this matters:**
-
- * `call` = **synchronous** — the caller waits. Like an HTTP request/response.
- * `cast` = **asynchronous** — fire and forget. Like sending an email.
- * Messages are processed **one at a time** — no race conditions, no locks, guaranteed.
- * The GenServer process is **isolated** — if it crashes, nothing else is affected.
-
- **For JS devs:** This is like a Redux store, but it's a real concurrent process
- that can handle requests from multiple callers simultaneously.
-
- **For Rails devs:** This replaces Rails cache + Redis for in-memory state,
- but it's built into the language and is process-safe by default.
-
## Processes: Supervisors — "Let It Crash"
What happens when a process crashes? In most languages, you write defensive code
@@ @@ -471,82 +358,24 @@ defmodule MyAppWeb.SearchLive do
socket
|> assign(:query, "")
|> assign(:results, [])
- |> assign(:loading, false)}
end
# User types in the search box
- def handle_event("search", %{"query" => query}, socket) do
- send(self(), {:do_search, query})
-
- {:noreply,
- socket
- |> assign(:query, query)
- |> assign(:loading, true)}
- end
-
# Search runs asynchronously
- def handle_info({:do_search, query}, socket) do
+ def handle_event("search", %{"query" => query}, socket) do
results = MyApp.Search.find(query)
{:noreply,
socket
|> assign(:results, results)
- |> assign(:loading, false)}
end
end
```
- ### What You DON'T Need
-
- With LiveView, you eliminate entire layers of your stack:
-
- | Traditional SPA | Phoenix LiveView |
- | ------------------------- | --------------------------------------- |
- | REST/GraphQL API | Not needed |
- | JSON serialization | Not needed |
- | Client-side state (Redux) | Server-side assigns |
- | API client (fetch/axios) | Not needed |
- | WebSocket library | Built in |
- | Client-side routing | Server-side routing |
- | JavaScript framework | Optional — only for JS-specific needs |
-
- The server is the source of truth. State lives in one place. Real-time features
- like live search, live validation, presence tracking — they come nearly for free.
-
- ### When You DO Need JavaScript
-
- LiveView isn't anti-JavaScript. It has **hooks** for when you need client-side behavior:
-
- * Rich text editors (TinyMCE, Trix)
- * Maps and charts (Leaflet, Chart.js)
- * Drag and drop
- * Clipboard, camera, etc.
-
- But the business logic stays on the server.
-
## Recap
- Three things that make Elixir different:
-
- **Pattern Matching:**
-
- * `=` is matching, not assignment
- * Function heads replace if/else chains
- * `with` chains operations with built-in error handling
- * Your code reads like a specification
-
- **Processes & Concurrency:**
-
- * Lightweight processes (~2KB each, millions possible)
- * Message passing instead of shared state — no locks, no race conditions
- * GenServer for stateful processes
- * Supervisors automatically restart crashed processes
-
- **LiveView:**
-
- * Real-time UI without writing JavaScript
- * Server renders HTML, sends only diffs over WebSocket
- * No API layer, no client state management, no JSON serialization
- * Built-in support for real-time features
+ * **Pattern matching** — function heads replace if/else, `with` chains operations with error handling
+ * **Processes** — lightweight (~2KB), isolated, supervised — "let it crash" instead of defensive code
+ * **LiveView** — real-time UI without JavaScript, server renders HTML diffs over WebSocket
**Next up:** Let's see these patterns in production code...
slides/01-intro.html +16 -38
@@ @@ -56,42 +56,42 @@
Huge thanks to Vianet for fully embracing Elixir and giving us the freedom to adopt it across our stack.
</p>
<div class="entity-grid">
- <div class="entity-card fragment">
+ <div class="entity-card">
<img src="logos/thefactory.png" alt="The Factory" class="entity-logo" />
<div class="entity-text">
<h4>The Factory</h4>
<p>Coworking space in Grand Rapids</p>
</div>
</div>
- <div class="entity-card fragment">
+ <div class="entity-card">
<img src="logos/puppies.png" alt="Puppies.com" class="entity-logo" />
<div class="entity-text">
<h4>Puppies.com</h4>
<p>Online puppy marketplace</p>
</div>
</div>
- <div class="entity-card fragment">
+ <div class="entity-card">
<img src="logos/roommates.png" alt="Roommates.com" class="entity-logo" />
<div class="entity-text">
<h4>Roommates.com</h4>
<p>Roommate &amp; room rental matching</p>
</div>
</div>
- <div class="entity-card fragment">
+ <div class="entity-card">
<img src="logos/orbitfour.png" alt="OrbitFour" class="entity-logo" />
<div class="entity-text">
<h4>OrbitFour</h4>
<p>Domain registrar &mdash; no upselling</p>
</div>
</div>
- <div class="entity-card fragment">
+ <div class="entity-card">
<img src="logos/reputablerooms.png" alt="Reputable Rooms" class="entity-logo" />
<div class="entity-text">
<h4>Reputable Rooms</h4>
<p>Verified room &amp; roommate search</p>
</div>
</div>
- <div class="entity-card fragment">
+ <div class="entity-card">
<img src="logos/rentprotect.png" alt="RentProtect" class="entity-logo" />
<div class="entity-text">
<h4>RentProtect</h4>
@@ @@ -131,47 +131,25 @@
</aside>
</section>
- <!-- SLIDE 6: Erlang -->
+ <!-- SLIDE 6: Erlang & The BEAM -->
<section>
- <h2>Erlang <span class="muted" style="font-weight: 400;">(1986)</span></h2>
- <p>Built by <strong>Ericsson</strong> for telecom switches &mdash; systems that literally <strong>could not go down</strong>.</p>
- <p class="fragment small muted">Think about what a phone network requires:</p>
+ <h2>Erlang &amp; The BEAM</h2>
+ <p>Built by <strong>Ericsson</strong> in 1986 for telecom switches &mdash; systems that literally <strong>could not go down</strong>.</p>
+ <p class="fragment small muted">The BEAM VM was designed from scratch for this:</p>
<ul>
- <li class="fragment">Millions of simultaneous calls</li>
- <li class="fragment">A single dropped call can't crash the entire system</li>
- <li class="fragment">Update software <strong>without</strong> hanging up on everyone</li>
- <li class="fragment">99.9999999% uptime &mdash; <em>~31ms of downtime per year</em></li>
- </ul>
- <p class="fragment small muted" style="margin-top: 0.8em;">
- Battle-tested for <strong>40 years</strong> in phone networks, banking, and messaging.
- </p>
-
- <aside class="notes">
- Erlang was created by Ericsson in 1986 for telecom switches — systems that literally could not go down. Think about what a phone network requires: millions of simultaneous calls, where a single dropped call can't crash the whole system. You need to update software without disconnecting everyone. And the uptime target? Nine nines — that's about 31 milliseconds of downtime per year. This wasn't theoretical — Erlang has been battle-tested for 40 years in phone networks, banking, and messaging systems.
- </aside>
- </section>
-
- <!-- SLIDE 7: The BEAM -->
- <section>
- <h2>The BEAM</h2>
- <p>Erlang's virtual machine &mdash; designed from scratch for concurrent, networked applications.</p>
- <ul>
- <li class="fragment"><strong>One process per request</strong> &mdash; ~2KB each, millions at once</li>
+ <li class="fragment"><strong>Lightweight processes</strong> &mdash; ~2KB each, millions at once</li>
<li class="fragment"><strong>Preemptive scheduling</strong> &mdash; no request can starve others</li>
<li class="fragment"><strong>Per-process GC</strong> &mdash; no stop-the-world pauses</li>
- <li class="fragment"><strong>Crash isolation</strong> &mdash; a single process can't take down the app</li>
+ <li class="fragment"><strong>Crash isolation</strong> &mdash; one process can't take down the app</li>
</ul>
- <p class="fragment muted" style="margin-top: 1em; font-style: italic;">
- Arguably the best runtime ever built for this problem.<br/>
+ <p class="fragment muted" style="margin-top: 0.8em; font-style: italic;">
+ Battle-tested for 40 years. Nine nines of uptime.<br/>
It just wasn't fun to work with...
</p>
<aside class="notes">
- The BEAM is the runtime, and it's fundamentally different from V8 or the JVM.<br>
- Every request gets its own process — about 2KB each — so you can handle millions concurrently. Not threads, not coroutines — real isolated processes.<br>
- The scheduler is preemptive. In Node, one CPU-heavy request blocks the event loop for everyone. On the BEAM, the scheduler gives every process a fair slice — no one gets starved.<br>
- Garbage collection happens per-process. In Node or the JVM, GC can pause your entire app. Here, only the process being collected pauses — everyone else keeps running.<br>
- And crash isolation — if one request hits a bug, that process dies alone. The rest of your app doesn't even notice. Compare that to an unhandled exception crashing your Node process.<br>
+ Erlang was created by Ericsson in 1986 for telecom switches — systems that literally could not go down. Think phone networks: millions of simultaneous calls, where one dropped call can't crash the whole system. The uptime target? Nine nines — about 31 milliseconds of downtime per year.<br><br>
+ The BEAM is the runtime, and it's fundamentally different from V8 or the JVM. Every request gets its own process — about 2KB each — millions concurrently. The scheduler is preemptive — in Node, one heavy request blocks the event loop; on the BEAM, every process gets a fair slice. GC happens per-process — no stop-the-world pauses. And crash isolation — if one request hits a bug, that process dies alone. The rest of your app doesn't notice.<br><br>
The catch? Erlang syntax is... an acquired taste. That's where Elixir comes in.
</aside>
</section>
slides/03-real-world.html +45 -191
@@ @@ -100,34 +100,24 @@
<p class="muted small">Stream records in chunks &mdash; constant memory usage</p>
<pre><code class="language-elixir" data-trim>
- defmodule Marketing.CampaignSchedulerWorker do
- use Oban.Worker, queue: :default
- @chunk_size 2_000
-
- def perform(%Oban.Job{args: args}) do
- campaign = Campaigns.get_campaign!(args["campaign_id"])
-
- Repo.transaction(fn ->
- Repo.stream(Campaigns.member_ids_query(campaign), max_rows: @chunk_size)
- |> Stream.chunk_every(@chunk_size)
- |> Enum.each(&schedule_batch(&1, campaign))
- end, timeout: :infinity)
- end
-
- defp schedule_batch(member_ids, campaign) do
- Enum.map(member_ids, fn member_id ->
- %{campaign_id: campaign.id, member_id: member_id}
- |> CampaignMailerWorker.new()
+ def perform(%Oban.Job{args: args}) do
+ campaign = Campaigns.get_campaign!(args["campaign_id"])
+
+ # Stream from DB → chunk → bulk insert jobs
+ Repo.transaction(fn ->
+ Repo.stream(member_ids_query(campaign), max_rows: 2_000)
+ |> Stream.chunk_every(2_000)
+ |> Enum.each(fn member_ids ->
+ member_ids
+ |> Enum.map(&amp;build_mailer_job(&amp;1, campaign))
+ |> Oban.insert_all()
end)
- |> Oban.insert_all()
- end
+ end, timeout: :infinity)
end
</code></pre>
<aside class="notes">
- You can't load 500k records into memory at once. Repo.stream gives us a lazy stream from the database — it uses a database cursor to fetch rows lazily. The Repo.transaction wrapper is required because the cursor needs the database connection to stay open for the entire iteration. Without it, the connection goes back to the pool and the cursor is lost.<br><br>
- Stream.chunk_every breaks the stream into chunks of 2,000. Each chunk becomes 2,000 individual email jobs inserted into Oban. Constant memory usage regardless of how many records we process.<br><br>
- The ampersand in &schedule_batch is Elixir's capture operator — it's shorthand for an anonymous function. For the JS folks, it's like writing (chunk) => scheduleBatch(chunk, campaign). The &1 just means "the first argument."
+ You can't load 500k records into memory at once. Repo.stream gives us a lazy stream — a database cursor that fetches rows lazily. Stream.chunk_every breaks it into chunks of 2,000. Each chunk becomes 2,000 individual email jobs inserted into Oban. Constant memory usage regardless of how many records we have. The Repo.transaction wrapper is required because the database cursor needs the connection to stay open for the full iteration.
</aside>
</section>
@@ @@ -193,39 +183,6 @@ end
</aside>
</section>
- <section>
- <h2>What's a GenServer?</h2>
- <p>A <strong>long-running process</strong> that holds state and handles messages one at a time.</p>
- <ul>
- <li class="fragment">Think of it as a <strong>tiny server inside your app</strong> with its own memory</li>
- <li class="fragment">Other code sends it messages &mdash; it processes them <strong>sequentially</strong></li>
- <li class="fragment">It stays alive for the lifetime of your application</li>
- <li class="fragment">If it crashes, the supervisor restarts it automatically</li>
- </ul>
-
- <aside class="notes">
- Before we talk about why we used a GenServer, let's explain what one is. A GenServer is a long-running process that holds state and processes messages one at a time. Think of it like a tiny server living inside your app. Other code sends it messages, it handles them sequentially — so there are no race conditions by design. It stays alive as long as your app is running, and if it crashes, the supervisor brings it right back. For the JS folks, think of a Web Worker with a built-in message queue. For Rails folks, think of a background process that's in-memory and automatically restarted.
- </aside>
- </section>
-
- <section>
- <h2>Why GenServer?</h2>
- <p class="muted small">Why not Redis, a database, or an in-memory variable?</p>
- <ul>
- <li class="fragment"><strong>In-process</strong> &mdash; no network round-trips like Redis</li>
- <li class="fragment"><strong>No race conditions</strong> &mdash; messages processed one at a time, by design</li>
- <li class="fragment"><strong>Fast</strong> &mdash; no database writes, no serialization overhead</li>
- <li class="fragment"><strong>Supervised</strong> &mdash; crashes and restarts automatically with fresh state</li>
- </ul>
- <p class="fragment" style="font-size: 0.8em; margin-top: 0.8em; color: #555;">
- Saves a network call for <strong>every single send</strong><br> at 500k emails, that's a lot of saved round-trips.
- </p>
-
- <aside class="notes">
- So why a GenServer for the rate limiter? It's in-process — no network hop to Redis for every email. No race conditions because messages are sequential. It's fast — no database writes, no serialization. And it's supervised — if something goes wrong, the supervisor restarts it. At 500k emails, a Redis round-trip per email would add serious latency. The GenServer handles it all in-memory with zero overhead.
- </aside>
- </section>
-
<!-- =============================================
SECTION 3.2: OrbitFour Domain Registry
============================================= -->
@@ @@ -248,36 +205,16 @@ end
<section>
<h2>What is OrbitFour?</h2>
- <p>A <strong>domain registrar</strong> &mdash; customers buy and manage domain names like <code>.com</code> and <code>.net</code>.</p>
+ <p>A <strong>domain registrar</strong> &mdash; think GoDaddy, but built by a small team in West Michigan.</p>
<ul>
- <li class="fragment">Communicates directly with <strong>Verisign</strong> to register domains</li>
- <li class="fragment">Handles the full lifecycle: search &rarr; register &rarr; DNS &rarr; renew &rarr; transfer</li>
- <li class="fragment">Customer portal, billing, and marketing site &mdash; all in one</li>
+ <li class="fragment">Talks directly to <strong>Verisign</strong> via persistent TCP &mdash; EPP protocol</li>
+ <li class="fragment">Full lifecycle: search &rarr; register &rarr; DNS &rarr; renew &rarr; transfer</li>
+ <li class="fragment">Billing, RDAP/WHOIS, customer portal, marketing site &mdash; <strong>all one Phoenix app</strong></li>
+ <li class="fragment"><strong>16 background workers</strong> &mdash; syncing, renewals, health checks</li>
</ul>
- <p class="fragment muted small" style="margin-top: 0.8em;">
- Think GoDaddy or Namecheap &mdash; but built by a small team in West Michigan.
- </p>
<aside class="notes">
- OrbitFour is Vianet's domain registrar. Customers come to the site, search for a domain, buy it, manage DNS, renew it — the whole lifecycle. Under the hood, it talks directly to Verisign over a persistent TCP connection to actually register .com and .net domains. Think GoDaddy or Namecheap, but built by a small team right here in West Michigan. And it's all one Elixir app.
- </aside>
- </section>
-
- <section>
- <h2>Lots of Moving Parts</h2>
- <p class="muted small">but a single Phoenix app handles <strong>everything</strong></p>
- <ul>
- <li class="fragment"><strong>EPP</strong> &mdash; register, renew, transfer, and manage domains</li>
- <li class="fragment"><strong>RDAP &amp; WHOIS</strong> &mdash; public lookup APIs (RFC-compliant)</li>
- <li class="fragment"><strong>Billing</strong> &mdash; Stripe payments, invoicing, renewals</li>
- <li class="fragment"><strong>DNS</strong> &mdash; zone management, DNSSEC, nameservers</li>
- <li class="fragment"><strong>Portal</strong> &mdash; LiveView dashboard, 2FA, account management</li>
- <li class="fragment"><strong>Marketing site</strong> &mdash; public pages, domain search, SEO</li>
- <li class="fragment"><strong>16 background workers</strong> &mdash; syncing, renewals, health checks, cleanup</li>
- </ul>
-
- <aside class="notes">
- Before we talk about the challenge, look at what this single Elixir app handles. EPP protocol for talking to Verisign over persistent TCP. RDAP and WHOIS public APIs. Stripe billing with invoicing and auto-renewals. DNS zone management with DNSSEC. A full customer portal built in LiveView with two-factor auth. The marketing website. And 16 background workers handling everything from domain syncing to SSL provisioning. In most stacks, this would be 4 or 5 separate services. Here it's one app, one deployment, one supervision tree.
+ OrbitFour is Vianet's domain registrar. Customers search for a domain, buy it, manage DNS, renew it — the whole lifecycle. Under the hood, it talks directly to Verisign over a persistent TCP connection. And here's what's impressive: one Phoenix app handles everything. EPP protocol, RDAP and WHOIS public APIs, Stripe billing, DNS zone management, a full LiveView customer portal, the marketing website, and 16 background workers. In most stacks, this would be 4 or 5 separate services. Here it's one app, one deployment, one supervision tree.
</aside>
</section>
@@ @@ -395,100 +332,39 @@ end
<section>
<h2>LiveView: Assign Async</h2>
- <p class="muted small">Loading data from three databases asynchronously</p>
-
- <pre class="small-code"><code class="language-elixir" data-trim>
- def mount(params, _session, socket) do
- 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
- </code></pre>
-
-
- <aside class="notes">
- Here's a real LiveView mount function. It loads admin lists from three different databases asynchronously using assign_async. All three queries run concurrently, and loading states are automatic — the template gets spinners for free while data loads.
- </aside>
- </section>
-
- <section>
- <h2>Using Async Results</h2>
- <p class="muted small">The template handles loading and success states automatically</p>
+ <p class="muted small">Load from three databases concurrently &mdash; loading states are automatic</p>
+
+ <pre class="tiny-code"><code class="language-elixir" data-trim data-line-numbers="1-13|15-23">
+ # In mount — load data asynchronously from three databases
+ socket
+ |> assign_async(
+ [:puppies_admins, :roommates_admins, :rr_admins],
+ fn ->
+ {:ok, %{
+ puppies_admins: Puppies.Admins.get_active_admins(),
+ roommates_admins: Roommates.Admins.get_admins(),
+ rr_admins: ReputableRooms.Admins.get_admins()
+ }}
+ end
+ )
- <pre class="small-code"><code class="language-elixir" data-trim>
+ # In template — spinner while loading, then render
&lt;.async_result :let={admins} assign={@puppies_admins}&gt;
- &lt;:loading&gt;
- &lt;.spinner /&gt;
- &lt;/:loading&gt;
-
+ &lt;:loading&gt;&lt;.spinner /&gt;&lt;/:loading&gt;
&lt;.input
type="select"
- label="Puppies admin account"
- options={Enum.map(admins, &{&1.name, &1.id})}
+ label="Puppies admin"
+ options={Enum.map(admins, &amp;{&amp;1.name, &amp;1.id})}
/&gt;
&lt;/.async_result&gt;
</code></pre>
<p class="fragment" style="font-size: 0.7em; color: #555;">
- Loading spinner &rarr; data arrives &rarr; dropdown renders.<br/>
<strong>No loading state management. No useEffect. No fetch.</strong>
</p>
<aside class="notes">
- Here's how you consume the async result in the template. The async_result component gives you a loading slot for a spinner, and when the data arrives, it renders the content with the resolved value. No useState, no useEffect, no loading boolean you have to manage. LiveView handles it all. Compare this to React where you'd need useState for the data, useEffect for the fetch, a loading boolean, error handling — all wired up manually.
- </aside>
- </section>
-
- <section>
- <h2>LiveView: Async Charts</h2>
- <p class="muted small">Data loads asynchronously, pushes chart updates to the browser</p>
-
- <pre class="small-code"><code class="language-elixir" data-trim>
- def mount(_params, _session, socket) do
- socket =
- socket
- |> assign(stats: AsyncResult.loading())
- |> assign_async(:site_stats, fn ->
- {:ok, %{site_stats: Stats.latest_by_site("roommates")}}
- end)
- |> start_async(:stats, fn -> Stats.load_stats("havers", "7") end)
-
- {:ok, socket}
- end
-
- # Async task completes → update the chart
- def handle_async(:stats, {:ok, fetched_stats}, socket) do
- data = %{stats: fetched_stats, title: "Listings created per hour"}
- socket =
- socket
- |> assign(stats: AsyncResult.ok(socket.assigns.stats, fetched_stats))
- {:noreply, push_event(socket, "graph", data)}
- end
-
- # User changes 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
- </code></pre>
-
- <aside class="notes">
- Stats dashboard with async charts. Page loads with a spinner. When the async task completes, handle_async fires and pushes chart data to the browser. User changes a filter — new async task starts, spinner appears, chart updates when data arrives. No REST API, no fetch calls, no loading state management library. All built into LiveView.
+ Here's assign_async in action. The mount function loads admin lists from three different databases concurrently. Loading states are automatic — the template gets spinners for free. When data arrives, the async_result component renders the content. Compare this to React: you'd need useState, useEffect, a loading boolean, error handling — all wired up manually. Here LiveView handles it all.
</aside>
</section>
@@ @@ -507,44 +383,22 @@ end
<section>
<h2>What We Covered</h2>
- <p class="muted">Three parts:</p>
<ol>
- <li class="fragment"><strong>Why Elixir?</strong> &mdash; The BEAM, 40 years of reliability, and why it matters for web devs</li>
- <li class="fragment"><strong>The Language</strong> &mdash; Pattern matching, processes, supervision, and LiveView</li>
- <li class="fragment"><strong>Real-World Code</strong> &mdash; 500k emails, a domain registrar, and a multi-site admin</li>
+ <li class="fragment"><strong>Why Elixir?</strong> &mdash; The BEAM, 40 years of reliability</li>
+ <li class="fragment"><strong>The Language</strong> &mdash; Pattern matching, processes, supervision, LiveView</li>
+ <li class="fragment"><strong>Real-World Code</strong> &mdash; 500k emails, a domain registrar, multi-site admin</li>
</ol>
-
- <aside class="notes">
- Quick recap — three parts. First, why Elixir exists: the BEAM, 40 years of telecom reliability, and why that matters for web developers. Then the language: pattern matching, processes, supervision, and LiveView — all hands-on in LiveBook. And finally, real production code from Vianet: half a million emails, a domain registrar consolidated from five services, and a multi-site admin dashboard.
- </aside>
- </section>
-
- <section>
- <h2>What Elixir Gives You</h2>
- <ul>
- <li class="fragment">State lives in <strong>processes</strong>, not shared memory</li>
- <li class="fragment">Errors are handled <strong>by design</strong>, not by hope</li>
+ <ul style="margin-top: 1em; list-style: none; padding: 0;">
<li class="fragment">Concurrency is the <strong>default</strong>, not an afterthought</li>
+ <li class="fragment">Errors are handled <strong>by design</strong>, not by hope</li>
<li class="fragment">Real-time features come <strong>for free</strong></li>
</ul>
-
- <aside class="notes">
- So what does Elixir actually give you? State lives in processes, not shared memory — no locks, no race conditions. Errors are handled by design — supervisors restart crashed processes, pattern matching makes every case explicit. Concurrency is the default — the BEAM was built for millions of simultaneous connections. And real-time features come for free with LiveView and Phoenix Channels.
- </aside>
- </section>
-
- <section>
- <h2>One Unified Platform</h2>
- <p class="fragment" style="font-size: 0.9em; color: #555; margin-bottom: 0.8em;">
- These aren't separate tools bolted together.<br/>
- They're all <strong>built into the language and framework</strong>.
- </p>
<p class="fragment muted small" style="margin-top: 0.8em; font-style: italic;">
It's a different paradigm &mdash; and once it clicks, it's hard to go back.
</p>
<aside class="notes">
- This is the key takeaway. Everything we've seen today — supervision, GenServer, pattern matching, concurrency, LiveView — these aren't libraries you npm install or gems you add. They're built into the language and framework from day one. They all work together because they were designed together. It's a fundamentally different way to build web applications. And once it clicks — once you see how processes, supervision, and pattern matching fit together — it's really hard to go back.
+ Quick recap. Why Elixir: the BEAM, 40 years of telecom reliability. The language: pattern matching, processes, supervision, and LiveView — all hands-on. Real production code: half a million emails, a domain registrar consolidated from five services, a multi-site admin dashboard. The key takeaway: concurrency, fault tolerance, and real-time features aren't libraries you bolt on — they're built into the language. It's a fundamentally different way to build web applications, and once it clicks, it's really hard to go back.
</aside>
</section>