Building a Simple Chat App with Elixir

[Kostas Mokas] | Dec 4, 2024

What is Elixir?

Elixir is a powerful functional programming language built for scalability and maintainability. In this post, we’ll create a simple chat application to explore how Elixir handles distributed systems, concurrency, and fault-tolerance.


Why Elixir?

Elixir runs on the Erlang Virtual Machine (BEAM), renowned for its ability to handle millions of lightweight processes with ease. This makes Elixir ideal for building fault-tolerant systems like chat apps. Here’s why developers love it:

  1. Concurrency: Simplifies running multiple tasks simultaneously.
  2. Fault-tolerance: Built-in supervision trees restart failed processes.
  3. Scalability: Horizontal scaling across nodes is seamless.
  4. Developer Productivity: Features like readable syntax and great tooling.

Setting Up the Project

First, install Elixir if you haven’t already. Once that’s done, create a new project with the mix tool:

mix new chat --sup

What’s Happening Here?

  • mix: Elixir’s build tool^[Mix].
  • new: Command to create a new project.
  • chat: The name of our project.
  • --sup: Adds a supervisor to manage processes.

Navigate into your project directory and test it:

cd chat
mix test

You should see output like this:

Compiling 2 files (.ex)
Generated chat app
..
Finished in 0.01 seconds (0.00s async, 0.01s sync)
1 doctest, 1 test, 0 failures

Adding a Supervisor

Supervisors^[Supervisor] are essential in Elixir for building fault-tolerant systems. They manage the lifecycle of processes, restarting them if something goes wrong.

Let’s update lib/chat/application.ex to include a Task.Supervisor:

# lib/chat/application.ex
defmodule Chat.Application do
	@moduledoc false
	use Application

	@impl true
	def start(_type, _args) do
		children = [
			{Task.Supervisor, name: Chat.TaskSupervisor}
		]

	    opts = [strategy: :one_for_one, name: Chat.Supervisor]
	    Supervisor.start_link(children, opts)
	  end
end

Here, we’re setting up a Task.Supervisor under the Chat.Supervisor tree. This will allow us to dynamically spawn tasks later.


Creating the Chat Module

Now, let’s define the Chat module and write a function to handle incoming messages:

# lib/chat.ex
defmodule Chat do
	@moduledoc false

	def receive_message(message) do
		IO.puts message
	end
end

Easy! Isn’t it?
What does it do? Simple: it takes the message we send and prints it directly to the terminal.

But receiving messages is only half the story. We also need to send them! Let’s add a function for that.


Sending Messages

To send messages, we’ll use a supervised task. Here’s how we define the send_message/2 function:

defmodule Chat do
  ...

  def send_message(recipient, message) do
    spawn_task(__MODULE__, :receive_message, recipient, [message])
  end

  def spawn_task(module, fun, recipient, args) do
    recipient
    |> remote_supervisor()
    |> Task.Supervisor.async(module, fun, args)
    |> Task.await()
  end

  defp remote_supervisor(recipient) do
    {Chat.TaskSupervisor, recipient}
  end
end

How It Works:

  1. send_message/2: Sends a message to a recipient by spawning a task.
  2. spawn_task/4: Spawns a supervised task on the recipient’s node and waits for it to complete.
  3. remote_supervisor/1: Locates the Task.Supervisor on the recipient’s node.

Running the App

Elixir makes it easy to run distributed applications. First, start an interactive shell (IEx) with a node name:

iex --sname alex@localhost -S mix

In a second terminal, start another node:

iex --sname kate@localhost -S mix

Now, send a message from one node to the other:

Chat.send_message(:kate@localhost, "Hello from Alex!")

You’ll see the message printed in the terminal where the kate node is running.


What Did We Build?

We’ve created a simple yet powerful chat app demonstrating how Elixir handles:

  • Process Supervision: Ensuring fault-tolerance with supervisors.
  • Concurrency: Using Task.Supervisor to manage tasks.
  • Distribution: Sending messages between nodes.

Conclusion

Elixir’s robust tooling and the Erlang VM’s strengths make it a perfect choice for distributed systems. By building this small application, you’ve seen how to set up a project, use supervisors, and communicate between nodes.

Happy coding with Elixir! 🚀