Speaker 1: You know, I want you to imagine walking into your
Speaker 1: absolute favorite coffee shop. You go to the exact same
Speaker 1: barista every single day, Right.
Speaker 2: They know your order by heart exactly.
Speaker 1: Or at least they should. But imagine every time you
Speaker 1: step up to the counter they just stare at you
Speaker 1: with this totally blank, uncomprehending expression.
Speaker 2: Oh, that would be incredibly frustrating.
Speaker 1: It would. They demand to see your ID, They pull
Speaker 1: up this massive, heavy filing cabinet, verify all your past orders,
Speaker 1: cross reference your credit history, and only then, like after
Speaker 1: all of that, do they suddenly remember that you just
Speaker 1: want your usual large flat white.
Speaker 3: It's exhausting just thinking about it, honestly. But you know,
Speaker 3: if we connect this to the bigger picture, what you
Speaker 3: are describing is exactly how the modern web operates. Yeah,
Speaker 3: we've basically built an entire global infrastructure on this massive
Speaker 3: illusion called statelessness.
Speaker 1: We really have, so welcome to the deep dives today.
Speaker 1: We are starting with this exact premise. Every single time
Speaker 1: you click a link or send a request, the serve
Speaker 1: basically suffers from total amnesia.
Speaker 4: It completely forgets who you are, right, it forgets what
Speaker 4: you're doing, and it has to frantically ask a database
Speaker 4: to reconstruct your entire identity before can send you a
Speaker 4: single web page.
Speaker 3: And I mean this approach of going stateless it really
Speaker 3: is what allowed the web to scale globally in the
Speaker 3: first place.
Speaker 1: Sure, because it frees up resources.
Speaker 3: Exactly by completely forgetting about you. The second and HTTP
Speaker 3: response is sent, servers free up their memory to handle
Speaker 3: the next user. But the architectural trade off there is severe.
Speaker 1: It really is.
Speaker 3: It created massive network latency, and it turned the database
Speaker 3: into this unavoidable central bottleneck. Every user action basically requires
Speaker 3: an expensive round trip ticket to a hard drive.
Speaker 1: Somewhere, which brings us to our mission. For this deep dive.
Speaker 1: We are exploring an early beta copy of a book
Speaker 1: by Lance Halverson. It's called Functional Web Development with alixer
Speaker 1: otp in.
Speaker 2: Phoenix, a fantastic read, by the way.
Speaker 1: Yeah, and the goal here is pretty radical. We're looking
Speaker 1: at how to build blazing fast, stateful applications that remember
Speaker 1: everything bypass the database entirely and kind of rethink web
Speaker 1: architecture from the ground up.
Speaker 3: And to prove this can actually work in production. The
Speaker 3: source material walks us through building a complex stateful game
Speaker 3: engine entirely in memory, like without touching a single database table.
Speaker 1: Okay, let's unpack this. To really understand how this works,
Speaker 1: we need to ground it in the game we are
Speaker 1: actually building. It's a two player grid based game called Islands.
Speaker 2: Yeah, kind of like a classic naval combat.
Speaker 1: Game, right exactly. So imagine two players, each with a
Speaker 1: ten x ten board cord. It's from a one down
Speaker 1: to j ten. Neither player can see the other's.
Speaker 3: Board, and you're placing islands of different shapes onto your grid, right,
Speaker 3: like an a tall, a single dot, an L shape,
Speaker 3: and S shape and a square yep. Once you set them,
Speaker 3: you take turns guessing coordinates on your opponent's board. If
Speaker 3: you guess right, you plant a palm tree on that island,
Speaker 3: get all the coordinates, and you force the island first
Speaker 3: to forrest all opponent islands.
Speaker 2: Wins.
Speaker 1: It is a perfect test case, honestly, because a game
Speaker 1: like this is inherently stateful. You have to remember where
Speaker 1: the pieces are, whose turn it is, what has already
Speaker 1: been guessed.
Speaker 2: You can't just forget everything between turns.
Speaker 1: Right. If you tried to build this with a traditional
Speaker 1: stateless web framework, say standard Rubyon rails or Django app,
Speaker 1: every single guess would require a round trip to a
Speaker 1: postgress database just to do one move exactly. It would
Speaker 1: have to lock the row, check the board, update the score,
Speaker 1: save the turn. It'd be incredibly heavy.
Speaker 3: Not to mention the concurrency issues if two things happen
Speaker 3: at once.
Speaker 1: So to build this without a database, the author introduces
Speaker 1: us to Alixer and the Erlang virtual machine, which is
Speaker 1: known as the Beam. The Beam is just phenomenal, it
Speaker 1: really is, and this is where my mind was blown.
Speaker 1: The Beam allows you to spawn millions of these ultra lightweight,
Speaker 1: long lived processes that hold data right in the server's.
Speaker 2: Memory, millions of them.
Speaker 1: But I have a major question here. If we have
Speaker 1: thousands of different games running simultaneously on one server, spunning
Speaker 1: millions of processes, doesn't the memory overhead completely kill the server?
Speaker 1: I mean, OS level threads are incredibly heavy.
Speaker 2: Well, that's the crucial distinction.
Speaker 3: Really, we aren't talking about OS threads here or even
Speaker 3: you know, Docker containers. A Beam process is an entirely
Speaker 3: different beast. How so, it starts at around two kilobytes
Speaker 3: of memory. It is unbelievably lightweight.
Speaker 1: Two kilobytes wow.
Speaker 3: Yeah, And the Beam has its own internal scheduler that
Speaker 3: handles context switching between these processes efficiently. It does this
Speaker 3: without ever asking the host operating system for permission.
Speaker 1: That's wild.
Speaker 3: It allocates a certain number of reductions or function calls
Speaker 3: to each process before forcing it to yield. That's actually
Speaker 3: how it handles millions of concurrent tasks without blocking the
Speaker 3: main thread.
Speaker 1: Okay, so they are cheap to make, But how do
Speaker 1: we prevent the data from getting scrambled?
Speaker 2: What do you mean?
Speaker 1: Well, in a normal web app, if two people click
Speaker 1: a button at the exact same millisecond, you get race conditions.
Speaker 1: Data gets overwritten unless you have a heady database.
Speaker 2: Lock right right?
Speaker 1: Is this setup sort of like having a goldfish teller
Speaker 1: at a bank who has to run back and check
Speaker 1: the main vault for every single transaction, versus having a
Speaker 1: dedicated teller who just keeps your specific file open right
Speaker 1: on their desk the whole time.
Speaker 3: Oh I love that analogy. Yes, it's exactly like the
Speaker 3: dedicated teller in Alixer. These dedicated tellers are called agents.
Speaker 3: Agents are simple, lightweight processes designed specifically to hold state,
Speaker 3: so they.
Speaker 1: Literally just sit there holding your data in memory exactly.
Speaker 2: And what's fascinating.
Speaker 3: Here is how they solve that race condition problem without
Speaker 3: database locks. They rely on a principle called single representation.
Speaker 1: Okay, what does that mean in practice?
Speaker 3: Well, a piece of data, let's say the status of
Speaker 3: Coordinate A one on Player one's board. It lives inside
Speaker 3: exactly one agent process, nowhere else in the entire system,
Speaker 3: and every single beam process has an exclusive built in mailbox.
Speaker 1: So wait, if fifty network requests arrive at the exact
Speaker 1: same millisecond asking to modify Coordinate A one, what.
Speaker 3: Happens in mailbox physically cues them up. The agent processes
Speaker 3: those messages sequentially, one by one. It processes message one,
Speaker 3: updates its internal state, then pulls message too.
Speaker 1: So there's no collision at all.
Speaker 2: None.
Speaker 3: Because the data has a single representation and a sequential mailbox,
Speaker 3: it naturally effortlessly prevents race conditions. The architecture itself makes
Speaker 3: a race condition mathematically impossible.
Speaker 1: That is incredibly elegant. And because we aren't using a database,
Speaker 1: we don't have to deal with object relational mapping or ORM.
Speaker 2: Right, you get to skip all of that overhead.
Speaker 1: There are no foreign keys, no joint tables, no M
Speaker 1: plus one cordy problems. The architecture composes the game entities
Speaker 1: using pure native Elixir data structures.
Speaker 2: Which eliminates an enormous layer of translation logic. It's so
Speaker 2: much cleaner.
Speaker 1: So an individual coordinate is an Elixir struct It just
Speaker 1: tracks a couple of specific things. A key for in
Speaker 1: island to say what island it belongs to, and a
Speaker 1: booleion for guests to say if it's been hit.
Speaker 2: Very simple.
Speaker 1: Yeah, then an island is just a list of those
Speaker 1: coordinate processes, and a board is a map that connects
Speaker 1: string names like A one to the specific coordinate process.
Speaker 3: Ye.
Speaker 1: You are literally just nesting native data structures.
Speaker 3: But here is the critical part, and this is where
Speaker 3: it gets really smart. Because each of these entities, the coordinates,
Speaker 3: the islands, the boards are individual agent processes.
Speaker 2: They are highly decoupled. Okay, if you update the A.
Speaker 3: One coordinate through the board process, the island process that
Speaker 3: also contains A one instantly sees the update.
Speaker 2: Why is that because the.
Speaker 3: Island doesn't hold a deeply nested copy of the coordinate data,
Speaker 3: It just holds the address the process ID pointing to
Speaker 3: that one specific coordinate agent.
Speaker 1: Okay, so we've solved the data layer we have perfect
Speaker 1: isolation and no race conditions. But having tens of thousands
Speaker 1: of isolated islands of data is useless without a logistics.
Speaker 2: Network, right, They need to communicate.
Speaker 1: Exactly How do they all coordinate if you have independent
Speaker 1: agents holding data, How does that not instantly turn into
Speaker 1: a tangled, chaotic mess of processes just shouting at each other.
Speaker 3: Well, that dilemma leads us directly to the next architectural layer.
Speaker 3: Having thousands of independent agents. It's like having thousands of
Speaker 3: employees who refuse to talk to anyone but their direct supervisor.
Speaker 1: That sounds like a nightmare. It is.
Speaker 3: You need a management structure. In the traditional tech world,
Speaker 3: when developers want to break up a massive application to
Speaker 3: scale it, they usually turn to service oriented architecture or microservices.
Speaker 1: Oh boy, micro services. I feel like every time an
Speaker 1: engineering team pivots to microservices, it starts with a hyped
Speaker 1: up whiteboard session and ends in absolute tears.
Speaker 2: The pain points are notorious. For sure.
Speaker 3: To get traditional microservices to talk to each other, you
Speaker 3: have to deal with incredibly complex network topologies.
Speaker 2: Oh the retrilogic alone, exactly. You have to write massive
Speaker 2: amounts of retrilogic just to handle network failures. Because the
Speaker 2: services are communicating over the open Internet via HTTP, testing
Speaker 2: them end to end is a nightmare.
Speaker 1: And local development is just as bad.
Speaker 3: Just to spin up a dev environment on your laptop,
Speaker 3: you might have to run Docker, Kubernetes, a dozen different ports,
Speaker 3: and configure external message cues like Kafka.
Speaker 2: It's brutal.
Speaker 1: But the architecture we're looking at offers a completely different path,
Speaker 1: using OTP, the Open Telecom platform, and a specific abstraction
Speaker 1: called a GEN server, which stands for Generic server.
Speaker 3: Right. A GEN server gives us the exact encapsulation and
Speaker 3: separation of concerns of a micro service, but it runs
Speaker 3: entirely inside our Elixir application.
Speaker 1: That is a huge difference.
Speaker 3: It lives inside the Beam virtual machine, so you get
Speaker 3: the benefits of a service oriented architecture, but with zero
Speaker 3: external infrastructure, no HTTP network ops, no JSON serialization, no
Speaker 3: Kubernetes cluster required.
Speaker 1: Here's where it gets really interesting. The book insists on
Speaker 1: a very specific repeatable code pattern for building these gen servers.
Speaker 1: He mandates that a client function wraps a mod function,
Speaker 1: which then triggers a callback.
Speaker 2: Yep, the standard OTP pattern.
Speaker 1: But why go through all these layers why not just
Speaker 1: have a function that reaches in and grabs.
Speaker 3: The data, because we have to remember how a genser
Speaker 3: actually holds state underneath the hood, a gen service basically
Speaker 3: just a recursive function running in an infinite loop.
Speaker 1: Okay.
Speaker 3: It receives a message, computes a new state, and then
Speaker 3: calls itself recursively with that new state. If you let
Speaker 3: other parts of your app interact directly with that recursive loop,
Speaker 3: you break the concurrency model.
Speaker 1: Ah see. So the client function is the public API.
Speaker 1: It's the safe interface that the rest of the app
Speaker 1: uses exactly.
Speaker 3: The client function sends a message to the gen server process.
Speaker 3: The gen server process receives it, triggers the internal callback function,
Speaker 3: processes the state change, and continues its loop safely. It
Speaker 3: completely isolates the internal state from the rest of the system.
Speaker 1: And as part of this messaging, we dive into the
Speaker 1: difference between a call and a cast.
Speaker 2: This is a really important distinction.
Speaker 1: Yeah. So, if I'm a process sending a message to
Speaker 1: a gens wherever, a call is synchronous, I knock on
Speaker 1: the door, ask a question, and I wait for the answer.
Speaker 1: A cast is a synchronous I throw the message over
Speaker 1: the wall and walk away, right, But the author strongly
Speaker 1: advocates for using call almost everywhere, even when you don't
Speaker 1: actually need the return data. Why why deliberately slow the
Speaker 1: system down by making it.
Speaker 3: Weight Because call creates a mechanical, unhackable system of back pressure.
Speaker 3: When you execute a call, the beam scheduler literally suspends
Speaker 3: the execution thread of the sender until the gen.
Speaker 1: Server replies, So it's a built in traffic light.
Speaker 3: Precisely, if a gen server is getting overwhelmed with messages,
Speaker 3: say a massive spike in user traffic, a call physically
Speaker 3: forces the senders to pause and wait their turn.
Speaker 2: It prevents the system from flooding.
Speaker 1: Because with a cast it would just keep piling up.
Speaker 3: Exactly with an asynchronous cast, you could bombard a process
Speaker 3: with a million messages in a second, completely fill up
Speaker 3: its mailbox memory, and crash the process. Using a call
Speaker 3: even if the reply is just an empty okay, keeps
Speaker 3: the entire memory footprint stable and predictable.
Speaker 1: Okay. That makes perfect sense for stability, and it's that
Speaker 1: stability that allows us to build out the game logic
Speaker 1: using the law of demeter.
Speaker 2: A great design principle.
Speaker 1: Yeah, it's where a process only talks to its immediate friends.
Speaker 1: The main game gen server doesn't reach deep down and
Speaker 1: directly tweak a.
Speaker 3: Coordinate because that would create brittle, highly coupled code. If
Speaker 3: the game process knows exactly how a coordinate works, any
Speaker 3: change to the coordinate breaks.
Speaker 2: The game right.
Speaker 1: So instead the architecture change the functions. The game process
Speaker 1: talks to the player process, the player process talks to
Speaker 1: the board map, and the board talks to the specific
Speaker 1: coordinate agent.
Speaker 2: It delegates the work down the chain exactly.
Speaker 1: If you want to change how a coordinate works, you
Speaker 1: only update the coordinate code.
Speaker 2: It's beautifully isolated.
Speaker 1: But wait, I have to push back here for a second.
Speaker 1: If we have ten thousand active games being played on
Speaker 1: our server, we have ten thousand game gen servers running.
Speaker 1: How does a player's web request actually their specific game process?
Speaker 2: Ah, the routing problem.
Speaker 1: Yeah, I know. Erlaying has a built in way to
Speaker 1: name processes using atoms, which are basically constant strings. But
Speaker 1: there's a massive warning in the source material about using
Speaker 1: atoms for dynamically generated games. Why can't we just name
Speaker 1: the process with an atom?
Speaker 3: Well, it's a fatal trap if you don't understand how
Speaker 3: the beam handles memory atoms are incredibly fast because under
Speaker 3: the hood, an atom is just an integer pointer to
Speaker 3: an entry in a C level string table.
Speaker 1: Okay, fast sounds good.
Speaker 3: It is, But because they are just pointers, atoms are
Speaker 3: never garbage collected. Once you create an atom, it stays
Speaker 3: in that table forever. And the Erling virtual machine has
Speaker 3: a hard unchangeable limit of just over one million atoms.
Speaker 1: Oh wow. So if you dynamically name every new game
Speaker 1: with an atom based on a user's ID.
Speaker 3: You will eventually hit that one million limit. And when
Speaker 3: the atom table fills up, the node doesn't just throw
Speaker 3: an exception, it violently crashes.
Speaker 2: The entire server process drops dead.
Speaker 3: Oh man, Yeah, it's a classic memory leak that causes
Speaker 3: three point zero zero AM emergency outages.
Speaker 1: Okay, but if atoms are a hard limit and they
Speaker 1: crash the node, why use them at all? Why not
Speaker 1: just use normal strings for everything? From the beginning? And
Speaker 1: what's the actual solution for routing to our game?
Speaker 3: We use a global name registry. Instead of registering the
Speaker 3: gen server with a primitive atom, we register it using
Speaker 3: a standard string like game call in player name.
Speaker 1: Because strings are garbage collected.
Speaker 2: Exactly, strings are garbage collected. The global registry keeps a
Speaker 2: dynamic track of which string points to which internal process
Speaker 2: idea across the entire server cluster.
Speaker 1: But isn't a global string registry a central bottleneck If
Speaker 1: every single request has to query one central registry to
Speaker 1: find its destination, aren't we just reinventing the database bottleneck
Speaker 1: and memory.
Speaker 3: It would be, except for how Elixir implements the registry.
Speaker 3: It uses ETS or laining term storage. It's an extremely
Speaker 3: fast concurrent in memory to built in c Okay, so
Speaker 3: it's optimized, very optimized. Multiple processes can read from an
Speaker 3: ETS table simultaneously without blocking each other, so the lookups
Speaker 3: are effectively instantaneous and highly concurrent. It completely bypasses the
Speaker 3: atom limit crash without introducing a bottleneck.
Speaker 1: Okay, so let's zoom out a bit. We have our
Speaker 1: data living cleanly in memory using agents. We have perfect
Speaker 1: robust communication and coordination using gen servers. But knowing where
Speaker 1: the pieces are is really only half the battle.
Speaker 2: Because a game has phases exactly, it has.
Speaker 1: The dimension of time. If player one tries to drop
Speaker 1: a bomb before player two is even joined the server,
Speaker 1: our perfect data layer is going to flawlessly record an
Speaker 1: illegal move. How do we build a bouncer that enforces
Speaker 1: the rules of time and sequence without writing a billion
Speaker 1: MESSIFL statements.
Speaker 3: This raise is an important question, and to see why
Speaker 3: this is so difficult, just look at the nightmare of
Speaker 3: modern front.
Speaker 2: End state management.
Speaker 1: To start on front end state.
Speaker 3: Right, You've got react use effect hooks, misfiring reduc stores,
Speaker 3: getting tangled components, rendering out of order. State fatigue is
Speaker 3: very real. When an application stays alive in memory, you
Speaker 3: have to manually track every single user interaction over time.
Speaker 1: Yeah, if modal A is open, disable button B, but
Speaker 1: only if user C is an admin.
Speaker 2: It devolves into a massive, tangled web of conditional logic.
Speaker 1: So how does this Elixir architecture prevent that exact same
Speaker 1: spaghetti logic on the back end.
Speaker 3: It avoids it by using another built in OTP behavior
Speaker 3: called gen statum, which stands for a generic state machine.
Speaker 3: A state machine acts as the ultimate uncompromising rule enforcer.
Speaker 3: It separates the state management logic entirely from the business logic.
Speaker 1: Let's walk through exactly how this is implemented for our game.
Speaker 1: The game starts in a state called initialized, and the
Speaker 1: brilliance of gen statum is how literal the code pattern is.
Speaker 1: You don't write a giant switch statement checking a state variable.
Speaker 2: No, you don't.
Speaker 1: You actually write a callback function and physically name it initialized.
Speaker 2: The function name is the state.
Speaker 1: So inside that initialized function, the code explicitly says the
Speaker 1: only permissible event here is adding a second player. If
Speaker 1: player two joins, the function returns a success topple and
Speaker 1: then instructs the state machine to transition the entire game
Speaker 1: into a completely new state called player's set.
Speaker 2: And this is the magic.
Speaker 3: Once you are in the players set state, the beam
Speaker 3: process actually changes which function handles the message loop. Really, yeah,
Speaker 3: the initialized function is no longer active. Any new messages
Speaker 3: are now routed exclusively to a function called player's set.
Speaker 1: But what if a rogue event tries to sneak in.
Speaker 1: What if there's a bug in the front end client
Speaker 1: and it sends a command trying to guess a coordinate
Speaker 1: while we are still in the initialized phase. If the
Speaker 1: initialized function only knows how to handle adding a second player,
Speaker 1: won't that unhandled message crash the process?
Speaker 3: That is where the catch all clause comes in, and
Speaker 3: it showcases the power of Elixer's pattern matching it's less
Speaker 3: like a standard else's statement and more like a coin sorting.
Speaker 1: Machine the coinsorder. Yeah.
Speaker 3: When an event drops in at the top, Elixra tries
Speaker 3: to match its shape against the defined rules.
Speaker 1: So if it's the exact right size for the ad
Speaker 1: player slot, it falls through and updates the state exactly.
Speaker 3: But if it's a misshapen rogue event, like guessing a
Speaker 3: coordinate too early, it slides past that slot. At the
Speaker 3: very bottom of your state function, you define a catch.
Speaker 1: All clause, and what does that do?
Speaker 3: The rogue event slides all the way down to this
Speaker 3: bottom slot, where it's safely rejected into the coin return.
Speaker 3: It physically cannot get stuck in the gears. The catch
Speaker 3: all simply returns an aerotople, denies the transition, and keeps
Speaker 3: the game safely in its current state.
Speaker 1: It completely eliminates the need for sprawling nested if statements.
Speaker 1: The rules of time and sequence are strictly enforced by
Speaker 1: the physical routing of the architecture itself.
Speaker 3: And the beauty is that the business logic doesn't have
Speaker 3: to care about time anymore. The logic for calculating a
Speaker 3: hit or a miss only exists inside the state where
Speaker 3: guessing is mathematically allowed.
Speaker 1: So what does this all mean? We started by talking
Speaker 1: about the amnesia of the mod web, the heavy reliance
Speaker 1: on databases just to remember who a user is, and
Speaker 1: the immense latency that causes.
Speaker 2: We covered a lot of ground today.
Speaker 1: We did, but by dissecting this architecture we've seen a
Speaker 1: completely different paradigm. We've seen how moving away from the
Speaker 1: database as a crutch and embracing stateful, concurrent processes can
Speaker 1: result in incredibly fast, robust applications.
Speaker 3: It proves that you can have the elegance of tiny,
Speaker 3: focused single purpose functions and compose them together to run
Speaker 3: massive complex systems entirely natively.
Speaker 2: No RMS, no.
Speaker 3: HTTP, microservice tangles across external networks, no JavaScript state fatigue,
Speaker 3: just clean, highly concurrent in memory processing managed by the beam.
Speaker 1: And that leads us to a final provocative thought for
Speaker 1: you to take away from this deep dive. We just
Speaker 1: walked through how to run a complex real time multiplayer
Speaker 1: game entirely in memory, using lightweight processes and state machines
Speaker 1: without ever touching a database.
Speaker 2: Which is the feat in itself.
Speaker 1: It is, but games are really just the ti up
Speaker 1: of the iceberg. What other standard notoriously database heavy business
Speaker 1: applications can be completely revolutionized by this architecture.
Speaker 3: It's a profound call to mullover. Just think about complex
Speaker 3: e commerce shopping carts that constantly query the database just
Speaker 3: hold five items.
Speaker 1: Or highly secure banking sessions where state must be.
Speaker 3: Perfectly preserved exactly, or live logistics networks tracking thousands of
Speaker 3: delivery trucks in real time. If we can simply allow
Speaker 3: the server to remember who we are and hold that
Speaker 3: state securely and concurrently in memory, the latency drops to
Speaker 3: almost zero.
Speaker 1: It's amazing to think about.
Speaker 2: The possibilities for scale and speed are enormous.
Speaker 1: Imagine your barista not only remembering your name and your
Speaker 1: exact order, but having it poured and ready the exact
Speaker 1: second you walk through the door without ever having to
Speaker 1: look at a ledger. That is the true power of
Speaker 1: a staatefle architecture. We want to warmly thank you for
Speaker 1: joining us on this deep dive today. Keep questioning the
Speaker 1: standard ways of doing things, and we'll see you next time.