Update speaker notes to match slide content across both decks

Torey Heinz committed Feb 23, 2026
commit 162b93cdb0b78f7f75f659a66752e327b00e74df
Showing 2 changed files with 10 additions and 11 deletions
slides/01-intro.html +2 -3
@@ @@ -147,8 +147,7 @@
</p>
<aside class="notes">
- Erlang was created by Ericsson in 1986 for telecom switches.<br>
- Battle-tested for <strong>40 years</strong> in phone networks, banking, and messaging.
+ 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>
@@ @@ -194,7 +193,7 @@
</p>
<aside class="notes">
- José Valim was on the Rails core team. He saw the power of the BEAM but wanted better ergonomics. Elixir gives you all of Erlang's superpowers — concurrency, fault tolerance, distribution — with a syntax that feels like Ruby. And you get the entire Erlang ecosystem for free.
+ José Valim was on the Rails core team. He saw the power of the BEAM but wanted better ergonomics. Elixir gives you all of Erlang's superpowers — concurrency, fault tolerance, distribution — with a syntax that feels like Ruby. The tooling is excellent: Mix is the build tool, Hex is the package manager, and ExUnit is the built-in test framework. And you get the entire Erlang ecosystem for free — Elixir doesn't replace Erlang, it runs on the same VM.
</aside>
</section>
slides/03-real-world.html +8 -8
@@ @@ -69,7 +69,7 @@
</ul>
<aside class="notes">
- The key constraint: AWS SES silently drops emails if you exceed your rate limit. No error, nothing. So we need a rate limiter that's rock-solid. We also need to handle bounces, complaints, and delivery notifications. Plus the ability to schedule and personalize half a million emails without blocking the web app.
+ Here's what makes this hard. Each email has to be sent individually so we can track opens, clicks, and bounces per recipient. We have to guarantee one and only one email per person — duplicates destroy trust. We need to handle bounces, complaints, and delivery notifications from AWS. Each email generates 3 to 4 API requests, so this has to run concurrently without blocking the web app. And the kicker: if you exceed your AWS SES rate limit, they silently drop your emails. No error, no warning, nothing. So we need a rate limiter that's rock-solid.
</aside>
</section>
@@ @@ -84,7 +84,7 @@
</ul>
<aside class="notes">
- Here's our solution. A scheduler worker pulls member IDs from the database in chunks of 2,000 and creates individual email jobs. Each mailer worker handles one email — validate, personalize, check the rate limiter, send. The rate limiter is a GenServer that enforces the AWS SES sending rate. All supervised.
+ Here's our solution. A scheduler worker queries recipients and creates individual Oban jobs — one per email. Oban is an Elixir job queue backed by Postgres, it picks up each job and triggers a mailer worker. Each mailer worker handles one email through a pipeline: validate, personalize, check the rate limiter, send. And the rate limiter is a GenServer that ensures we never exceed the AWS SES sending rate. Everything is supervised.
</aside>
</section>
@@ @@ -120,7 +120,7 @@ end
<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, liveMode). The &1 just means "the first argument."
+ 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."
</aside>
</section>
@@ @@ -152,7 +152,7 @@ end
</code></pre>
<aside class="notes">
- Each email goes through a pipeline with the `with` chain. Get the member, validate the email, get the campaign, reserve the record, check it hasn't been sent, check the rate limiter. If everything passes, send. The else block reads like a specification: rate exceeded? Snooze 1 second. Daily limit? Backoff up to an hour. Already sent? Discard. Every outcome is handled. No silent failures.
+ Each email goes through a pipeline using the `with` chain we saw in LiveBook. Get the member, validate the email, get the campaign, reserve a campaign email record to guarantee exactly-once delivery, then check the rate limiter. If everything passes, send. The else block reads like a specification: rate exceeded? Snooze 1 second. Daily limit hit? Back off up to an hour. Already sent? Discard. Invalid email? Discard. Every outcome is handled explicitly. No silent failures.
</aside>
</section>
@@ @@ -182,7 +182,7 @@ end
</code></pre>
<aside class="notes">
- This is the real rate limiter. It's a GenServer with a token bucket algorithm. The public API is just `ready?` — other code calls that. Internally, it tracks tokens and daily quota. Tokens refill over time. When a worker asks ready?, it checks tokens and daily quota. Thread-safe by design — GenServer processes messages one at a time.
+ This is the real rate limiter, simplified for the slide. It's a GenServer using a token bucket algorithm. The public API is just `ready?` — that's all other code needs to call. Internally, it tracks tokens that refill over time based on our SES sending rate. When a worker asks ready?, it refills tokens based on elapsed time, then checks if there's a token available. If yes, it decrements and says OK. If not, it says not ready. Thread-safe by design — the GenServer processes messages one at a time, so there's no way two workers can grab the same token.
</aside>
</section>
@@ @@ -228,7 +228,7 @@ end
<p>Taking ownership of our core functions</p>
<aside class="notes">
- Second system: OrbitFour, our domain registrar. This one communicates directly with Verisign over persistent TCP connections.
+ Second system: OrbitFour, our domain registrar. This is a story about taking ownership of your core functions — and how Elixir made that practical.
</aside>
</section>
@@ @@ -361,7 +361,7 @@ end
</code></pre>
<aside class="notes">
- The admin app's supervision tree connects to five databases simultaneously. Each Repo is a supervised connection pool to a different PostgreSQL database. Plus presence tracking, a scheduler, job queue, and in-memory cache. All in one tree.
+ Here's the admin app's supervision tree. It connects to five PostgreSQL databases simultaneously — each Repo is a supervised connection pool. Plus a connection monitor and presence tracking to see which admins are online. If any database connection drops, the supervisor handles it automatically. This is a simplified version — the full tree has more children, but this shows the pattern.
</aside>
</section>
@@ @@ -516,7 +516,7 @@ end
</p>
<aside class="notes">
- And here's the thing that makes Elixir special. All of these features — supervision, GenServer, pattern matching, concurrency, LiveView — they're not libraries you install or tools you bolt on. They're built into the language and framework from day one. 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. It's a fundamentally different way to build web applications, and once it clicks, it's really hard to go back.
+ 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.
</aside>
</section>