An experimental e-commerce platform exploring high-concurrency Erlang/Elixir services and real-time frontend components using Phoenix LiveView.
An experimental e-commerce service exploring Erlang/Elixir’s performance, stability, and real-time state synchronization.
Under the Hood: Building the Elixir Shop & Warehouse
Video: 1 Million Elixir Processes at 5$ VPS Demo
If you are coming from Java, Go, Python, or Node.js, Elixir will force you to unlearn how you think about concurrency and state.
This article is a 15-minute high-level tour of how the Elixir Shop and Warehouse services are architected. We will explore how BEAM concurrency primitives model real-world business logic and look at the design patterns that give this system its resilience.
1. The Bird’s-Eye Architecture
At a high level, the application is divided into two core domains: Shop (Customer-Facing) and Warehouse (Inventory & Operations).
graph TD
User([Customer Browser]) -->|WebSockets / LiveView| LV[Phoenix LiveView Process]
LV -->|Read/Write State| Cart[Cart GenServer Process]
Cart -->|Query Inventory| Registry[Elixir Registry]
Cart -->|Broadcast Changes| PubSub[Phoenix PubSub]
Warehouse[Warehouse GenServer] -->|Track Stock| DB[(PostgreSQL via Ecto)]
PubSub -->|Real-Time Broadcast| LV
Instead of a stateless API that queries a database on every click (typical in Node/Python), Elixir Shop uses Stateful, Persistent Processes.
- Every active customer has their own dedicated Cart process.
- The cart process lives in the server memory, keeping its state close to the CPU.
- Updates are streamed instantly back to the customer’s browser via persistent WebSockets using Phoenix LiveView.
2. The Core Language Primitives Used
To build this architecture, we leverage the battle-tested primitives of the OTP (Open Telecom Platform) framework:
GenServer (Generic Server)
Every shopper’s active cart is a separate GenServer process.
- State Preservation: The process holds its state (selected products, quantities, discount codes) in-memory.
- Sequential Mailbox: Because processes only process one message at a time, we get out-of-the-box race condition prevention. If a user clicks “Add to Cart” three times simultaneously, the requests are processed sequentially. No database transactions or locking mechanisms required!
defmodule ElixirShop.Cart do
use GenServer
# Client API
def add_item(cart_pid, item_id, quantity) do
GenServer.call(cart_pid, {:add_item, item_id, quantity})
end
# Server Callbacks
@impl true
def handle_call({:add_item, item_id, quantity}, _from, state) do
new_state = Map.update(state, item_id, quantity, &(&1 + quantity))
{:reply, :ok, new_state}
end
endRegistry
How do we find a specific customer’s cart process among millions of running processes? We use an Elixir Registry.
- The
Registryis a high-performance key-value store local to the BEAM instance. - It acts as a directory: when customer
user_4829performs an action, the registry points us to the specificPID(Process Identifier) of their cart.
Supervisors
Supervisors are processes whose sole job is to monitor other processes (workers or nested supervisors) and restart them if they crash.
[Shop Application Supervisor]
/ \
[Dynamic Supervisor] [Registry]
/
[Cart GenServer (user_1)]
[Cart GenServer (user_2)]- DynamicSupervisor: Since shoppers connect and disconnect dynamically, we use a
DynamicSupervisorto spin up cart processes on-demand and clean them up when idle. - Crash Isolation: If a cart process crashes due to a bug (e.g. attempting to parse a corrupted payload), only that single process dies. The supervisor immediately restarts it with an empty/clean state. Other shoppers are completely unaffected.
3. The Spirit of Elixir: Crucial BEAM Facts
To truly understand why Elixir is suited for this, you have to understand the BEAM virtual machine:
Fact #1: 1.5 KB Processes (Not OS Threads)
Traditional servers use OS threads (consuming 1–8 MB of stack space per thread) or async event loops.
- In the BEAM, processes are userspace green threads created and managed entirely by the VM.
- A BEAM process starts at just 1.5 KB of memory.
- You can easily spawn 1,000,000 concurrent processes on a single $5 VPS without breaking a sweat.
Fact #2: No Shared Memory = No Stop-the-World GC
In Java or Go, when the Garbage Collector (GC) runs, it eventually needs to pause execution to clean up heap memory, leading to latency spikes.
- In Elixir, processes share absolutely no memory. They communicate purely by copying messages.
- Consequently, garbage collection is per-process. Each process collects its own garbage independently. When a cart process finishes and dies, its entire memory space is reclaimed instantly without a GC sweep.
Fact #3: Preemptive Scheduling
In Node.js or Python, a single CPU-intensive loop can block the entire event loop, freezing the application for everyone.
- The BEAM scheduler is preemptive. It counts “reductions” (roughly equivalent to function calls) for each process.
- Once a process reaches 4,000 reductions (usually less than a millisecond), the scheduler pauses it, yields control to another process, and resumes it later.
- This guarantees hard real-time fairness and low latency, regardless of high loads or runaway loops.
Fact #4: The “Let It Crash” Philosophy
In other languages, you write defensive code wrapped in try/catch blocks to prevent the application from crashing.
- In Elixir, we embrace crashes. If something goes wrong, we let the process crash.
- The supervisor will restore the process to a known good state immediately.
- This prevents state corruption (e.g., leaving a half-completed database transaction or an inconsistent memory state), which is much harder to debug than a clean restart.
4. Server-Driven UI via Phoenix LiveView
The frontend of Elixir Shop doesn’t use React, Vue, or Angular. Instead, it uses Phoenix LiveView to achieve rich, single-page application interactivity.
- Persistent WebSocket Connection: When the customer opens the shop, a WebSocket connection is established.
- State on the Server: The LiveView process renders the initial HTML, and then waits for events.
- HTML Diffs: When a customer clicks a button, LiveView sends a tiny WebSocket message. The server updates the state, computes the difference in the HTML template, and sends back only the exact diffs (often just a few bytes).
- Zero Client Routing: There is no API layer, no JSON serialization, and no complex state synchronization logic between frontend and backend. It is all Elixir.